github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/db/lock.go (about)

     1  package db
     2  
     3  import (
     4  	"time"
     5  
     6  	"gopkg.in/mgo.v2"
     7  	"gopkg.in/mgo.v2/bson"
     8  )
     9  
    10  const (
    11  	LockCollection = "lock"
    12  	GlobalLockId   = "global"
    13  	LockTimeout    = time.Minute * 8
    14  )
    15  
    16  // Lock represents a lock stored in the database, for synchronization.
    17  type Lock struct {
    18  	Id       string    `bson:"_id"`
    19  	Locked   bool      `bson:"locked"`
    20  	LockedBy string    `bson:"locked_by"`
    21  	LockedAt time.Time `bson:"locked_at"`
    22  }
    23  
    24  // InitializeGlobalLock should be called once, at program initialization.
    25  func InitializeGlobalLock() error {
    26  	session, db, err := GetGlobalSessionFactory().GetSession()
    27  	if err != nil {
    28  		return err
    29  	}
    30  	defer session.Close()
    31  
    32  	// for safety's sake, check if it's there.  this will make this
    33  	// function idempotent
    34  	lock := Lock{}
    35  	err = db.C(LockCollection).Find(bson.M{"_id": GlobalLockId}).One(&lock)
    36  	if err != nil && err != mgo.ErrNotFound {
    37  		return err
    38  	}
    39  
    40  	// already exists
    41  	if lock.Id != "" {
    42  		return nil
    43  	}
    44  
    45  	return db.C(LockCollection).Insert(bson.M{"_id": GlobalLockId, "locked": false})
    46  }
    47  
    48  // WaitTillAcquireGlobalLock "spins" on acquiring the given database lock,
    49  // for the process id, until timeoutMS. Returns whether or not the lock was
    50  // acquired.
    51  func WaitTillAcquireGlobalLock(id string, timeoutMS time.Duration) (bool, error) {
    52  	startTime := time.Now()
    53  	for {
    54  		// if the timeout has been reached, we failed to get the lock
    55  		currTime := time.Now()
    56  		if startTime.Add(timeoutMS * time.Millisecond).Before(currTime) {
    57  			return false, nil
    58  		}
    59  
    60  		// attempt to get the lock
    61  		acquired, err := AcquireGlobalLock(id)
    62  		if err != nil {
    63  			return false, err
    64  		}
    65  		if acquired {
    66  			return true, nil
    67  		}
    68  
    69  		// sleep
    70  		time.Sleep(1000 * time.Millisecond)
    71  	}
    72  }
    73  
    74  // attempt to acquire the global lock of no one has it
    75  func setDocumentLocked(id string, upsert bool) (bool, error) {
    76  	session, db, err := GetGlobalSessionFactory().GetSession()
    77  	if err != nil {
    78  		return false, err
    79  	}
    80  	defer session.Close()
    81  
    82  	// for findAndModify-ing the lock
    83  
    84  	// timeout to check for
    85  	timeoutThreshold := time.Now().Add(-LockTimeout)
    86  
    87  	// construct the selector for the following cases:
    88  	// 1. lock is not held by anyone
    89  	// 2. lock is held but has timed out
    90  	selector := bson.M{
    91  		"_id": GlobalLockId,
    92  		"$or": []bson.M{{"locked": false}, {"locked_at": bson.M{"$lte": timeoutThreshold}}},
    93  	}
    94  
    95  	// change to apply to document
    96  	change := mgo.Change{
    97  		Update: bson.M{"$set": bson.M{
    98  			"locked":    true,
    99  			"locked_by": id,
   100  			"locked_at": time.Now(),
   101  		}},
   102  		Upsert:    upsert,
   103  		ReturnNew: true,
   104  	}
   105  
   106  	lock := Lock{}
   107  
   108  	// gets the lock if we can
   109  	_, err = db.C(LockCollection).Find(selector).Apply(change, &lock)
   110  
   111  	if err != nil {
   112  		return false, err
   113  	}
   114  	return lock.Locked, nil
   115  }
   116  
   117  // AcquireGlobalLock attempts to acquire the global lock if
   118  // no one has it or it's timed out. Returns a boolean indicating
   119  // whether the lock was acquired.
   120  func AcquireGlobalLock(id string) (bool, error) {
   121  	acquired, err := setDocumentLocked(id, false)
   122  
   123  	if err == mgo.ErrNotFound {
   124  		// in the case where no lock document exists
   125  		// this will return a duplicate key error if
   126  		// another lock contender grabs the lock before
   127  		// we are able to
   128  		acquired, err = setDocumentLocked(id, true)
   129  
   130  		// since we're upserting now, don't
   131  		// return any duplicate key errors
   132  		if mgo.IsDup(err) {
   133  			return acquired, nil
   134  		}
   135  		return acquired, err
   136  	}
   137  	return acquired, err
   138  }
   139  
   140  // ReleaseGlobalLock relinquishes the global lock for the given id.
   141  func ReleaseGlobalLock(id string) error {
   142  	session, db, err := GetGlobalSessionFactory().GetSession()
   143  	if err != nil {
   144  		return err
   145  	}
   146  	defer session.Close()
   147  
   148  	// will return mgo.ErrNotFound if the lock expired
   149  	return db.C(LockCollection).Update(
   150  		bson.M{"_id": GlobalLockId, "locked_by": id},
   151  		bson.M{"$set": bson.M{"locked": false}},
   152  	)
   153  }