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 }