| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: buildbot.test.test_locks -*- | |
| 2 | |
| 3 from twisted.python import log | |
| 4 from twisted.internet import reactor, defer | |
| 5 from buildbot import util | |
| 6 | |
| 7 if False: # for debugging | |
| 8 debuglog = log.msg | |
| 9 else: | |
| 10 debuglog = lambda m: None | |
| 11 | |
| 12 class BaseLock: | |
| 13 """ | |
| 14 Class handling claiming and releasing of L{self}, and keeping track of | |
| 15 current and waiting owners. | |
| 16 | |
| 17 @note: Ideally, we'd like to maintain FIFO order. The place to do that | |
| 18 would be the L{isAvailable()} function. However, this function is | |
| 19 called by builds/steps both for the first time, and after waking | |
| 20 them up by L{self} from the L{self.waiting} queue. There is | |
| 21 currently no way of distinguishing between them. | |
| 22 """ | |
| 23 description = "<BaseLock>" | |
| 24 | |
| 25 def __init__(self, name, maxCount=1): | |
| 26 self.name = name # Name of the lock | |
| 27 self.waiting = [] # Current queue, tuples (LockAccess, deferred) | |
| 28 self.owners = [] # Current owners, tuples (owner, LockAccess) | |
| 29 self.maxCount=maxCount # maximal number of counting owners | |
| 30 | |
| 31 def __repr__(self): | |
| 32 return self.description | |
| 33 | |
| 34 def _getOwnersCount(self): | |
| 35 """ Return the number of current exclusive and counting owners. | |
| 36 | |
| 37 @return: Tuple (number exclusive owners, number counting owners) | |
| 38 """ | |
| 39 num_excl, num_counting = 0, 0 | |
| 40 for owner in self.owners: | |
| 41 if owner[1].mode == 'exclusive': | |
| 42 num_excl = num_excl + 1 | |
| 43 else: # mode == 'counting' | |
| 44 num_counting = num_counting + 1 | |
| 45 | |
| 46 assert (num_excl == 1 and num_counting == 0) \ | |
| 47 or (num_excl == 0 and num_counting <= self.maxCount) | |
| 48 return num_excl, num_counting | |
| 49 | |
| 50 | |
| 51 def isAvailable(self, access): | |
| 52 """ Return a boolean whether the lock is available for claiming """ | |
| 53 debuglog("%s isAvailable(%s): self.owners=%r" | |
| 54 % (self, access, self.owners)) | |
| 55 num_excl, num_counting = self._getOwnersCount() | |
| 56 if access.mode == 'counting': | |
| 57 # Wants counting access | |
| 58 return num_excl == 0 and num_counting < self.maxCount | |
| 59 else: | |
| 60 # Wants exclusive access | |
| 61 return num_excl == 0 and num_counting == 0 | |
| 62 | |
| 63 def claim(self, owner, access): | |
| 64 """ Claim the lock (lock must be available) """ | |
| 65 debuglog("%s claim(%s, %s)" % (self, owner, access.mode)) | |
| 66 assert owner is not None | |
| 67 assert self.isAvailable(access), "ask for isAvailable() first" | |
| 68 | |
| 69 assert isinstance(access, LockAccess) | |
| 70 assert access.mode in ['counting', 'exclusive'] | |
| 71 self.owners.append((owner, access)) | |
| 72 debuglog(" %s is claimed '%s'" % (self, access.mode)) | |
| 73 | |
| 74 def release(self, owner, access): | |
| 75 """ Release the lock """ | |
| 76 assert isinstance(access, LockAccess) | |
| 77 | |
| 78 debuglog("%s release(%s, %s)" % (self, owner, access.mode)) | |
| 79 entry = (owner, access) | |
| 80 assert entry in self.owners | |
| 81 self.owners.remove(entry) | |
| 82 # who can we wake up? | |
| 83 # After an exclusive access, we may need to wake up several waiting. | |
| 84 # Break out of the loop when the first waiting client should not be awak
ened. | |
| 85 num_excl, num_counting = self._getOwnersCount() | |
| 86 while len(self.waiting) > 0: | |
| 87 access, d = self.waiting[0] | |
| 88 if access.mode == 'counting': | |
| 89 if num_excl > 0 or num_counting == self.maxCount: | |
| 90 break | |
| 91 else: | |
| 92 num_counting = num_counting + 1 | |
| 93 else: | |
| 94 # access.mode == 'exclusive' | |
| 95 if num_excl > 0 or num_counting > 0: | |
| 96 break | |
| 97 else: | |
| 98 num_excl = num_excl + 1 | |
| 99 | |
| 100 del self.waiting[0] | |
| 101 reactor.callLater(0, d.callback, self) | |
| 102 | |
| 103 def waitUntilMaybeAvailable(self, owner, access): | |
| 104 """Fire when the lock *might* be available. The caller will need to | |
| 105 check with isAvailable() when the deferred fires. This loose form is | |
| 106 used to avoid deadlocks. If we were interested in a stronger form, | |
| 107 this would be named 'waitUntilAvailable', and the deferred would fire | |
| 108 after the lock had been claimed. | |
| 109 """ | |
| 110 debuglog("%s waitUntilAvailable(%s)" % (self, owner)) | |
| 111 assert isinstance(access, LockAccess) | |
| 112 if self.isAvailable(access): | |
| 113 return defer.succeed(self) | |
| 114 d = defer.Deferred() | |
| 115 self.waiting.append((access, d)) | |
| 116 return d | |
| 117 | |
| 118 | |
| 119 class RealMasterLock(BaseLock): | |
| 120 def __init__(self, lockid): | |
| 121 BaseLock.__init__(self, lockid.name, lockid.maxCount) | |
| 122 self.description = "<MasterLock(%s, %s)>" % (self.name, self.maxCount) | |
| 123 | |
| 124 def getLock(self, slave): | |
| 125 return self | |
| 126 | |
| 127 class RealSlaveLock: | |
| 128 def __init__(self, lockid): | |
| 129 self.name = lockid.name | |
| 130 self.maxCount = lockid.maxCount | |
| 131 self.maxCountForSlave = lockid.maxCountForSlave | |
| 132 self.description = "<SlaveLock(%s, %s, %s)>" % (self.name, | |
| 133 self.maxCount, | |
| 134 self.maxCountForSlave) | |
| 135 self.locks = {} | |
| 136 | |
| 137 def __repr__(self): | |
| 138 return self.description | |
| 139 | |
| 140 def getLock(self, slavebuilder): | |
| 141 slavename = slavebuilder.slave.slavename | |
| 142 if not self.locks.has_key(slavename): | |
| 143 maxCount = self.maxCountForSlave.get(slavename, | |
| 144 self.maxCount) | |
| 145 lock = self.locks[slavename] = BaseLock(self.name, maxCount) | |
| 146 desc = "<SlaveLock(%s, %s)[%s] %d>" % (self.name, maxCount, | |
| 147 slavename, id(lock)) | |
| 148 lock.description = desc | |
| 149 self.locks[slavename] = lock | |
| 150 return self.locks[slavename] | |
| 151 | |
| 152 | |
| 153 class LockAccess(util.ComparableMixin): | |
| 154 """ I am an object representing a way to access a lock. | |
| 155 | |
| 156 @param lockid: LockId instance that should be accessed. | |
| 157 @type lockid: A MasterLock or SlaveLock instance. | |
| 158 | |
| 159 @param mode: Mode of accessing the lock. | |
| 160 @type mode: A string, either 'counting' or 'exclusive'. | |
| 161 """ | |
| 162 | |
| 163 compare_attrs = ['lockid', 'mode'] | |
| 164 def __init__(self, lockid, mode): | |
| 165 self.lockid = lockid | |
| 166 self.mode = mode | |
| 167 | |
| 168 assert isinstance(lockid, (MasterLock, SlaveLock)) | |
| 169 assert mode in ['counting', 'exclusive'] | |
| 170 | |
| 171 | |
| 172 class BaseLockId(util.ComparableMixin): | |
| 173 """ Abstract base class for LockId classes. | |
| 174 | |
| 175 Sets up the 'access()' function for the LockId's available to the user | |
| 176 (MasterLock and SlaveLock classes). | |
| 177 Derived classes should add | |
| 178 - Comparison with the L{util.ComparableMixin} via the L{compare_attrs} | |
| 179 class variable. | |
| 180 - Link to the actual lock class should be added with the L{lockClass} | |
| 181 class variable. | |
| 182 """ | |
| 183 def access(self, mode): | |
| 184 """ Express how the lock should be accessed """ | |
| 185 assert mode in ['counting', 'exclusive'] | |
| 186 return LockAccess(self, mode) | |
| 187 | |
| 188 def defaultAccess(self): | |
| 189 """ For buildbot 0.7.7 compability: When user doesn't specify an access | |
| 190 mode, this one is chosen. | |
| 191 """ | |
| 192 return self.access('counting') | |
| 193 | |
| 194 | |
| 195 | |
| 196 # master.cfg should only reference the following MasterLock and SlaveLock | |
| 197 # classes. They are identifiers that will be turned into real Locks later, | |
| 198 # via the BotMaster.getLockByID method. | |
| 199 | |
| 200 class MasterLock(BaseLockId): | |
| 201 """I am a semaphore that limits the number of simultaneous actions. | |
| 202 | |
| 203 Builds and BuildSteps can declare that they wish to claim me as they run. | |
| 204 Only a limited number of such builds or steps will be able to run | |
| 205 simultaneously. By default this number is one, but my maxCount parameter | |
| 206 can be raised to allow two or three or more operations to happen at the | |
| 207 same time. | |
| 208 | |
| 209 Use this to protect a resource that is shared among all builders and all | |
| 210 slaves, for example to limit the load on a common SVN repository. | |
| 211 """ | |
| 212 | |
| 213 compare_attrs = ['name', 'maxCount'] | |
| 214 lockClass = RealMasterLock | |
| 215 def __init__(self, name, maxCount=1): | |
| 216 self.name = name | |
| 217 self.maxCount = maxCount | |
| 218 | |
| 219 class SlaveLock(BaseLockId): | |
| 220 """I am a semaphore that limits simultaneous actions on each buildslave. | |
| 221 | |
| 222 Builds and BuildSteps can declare that they wish to claim me as they run. | |
| 223 Only a limited number of such builds or steps will be able to run | |
| 224 simultaneously on any given buildslave. By default this number is one, | |
| 225 but my maxCount parameter can be raised to allow two or three or more | |
| 226 operations to happen on a single buildslave at the same time. | |
| 227 | |
| 228 Use this to protect a resource that is shared among all the builds taking | |
| 229 place on each slave, for example to limit CPU or memory load on an | |
| 230 underpowered machine. | |
| 231 | |
| 232 Each buildslave will get an independent copy of this semaphore. By | |
| 233 default each copy will use the same owner count (set with maxCount), but | |
| 234 you can provide maxCountForSlave with a dictionary that maps slavename to | |
| 235 owner count, to allow some slaves more parallelism than others. | |
| 236 | |
| 237 """ | |
| 238 | |
| 239 compare_attrs = ['name', 'maxCount', '_maxCountForSlaveList'] | |
| 240 lockClass = RealSlaveLock | |
| 241 def __init__(self, name, maxCount=1, maxCountForSlave={}): | |
| 242 self.name = name | |
| 243 self.maxCount = maxCount | |
| 244 self.maxCountForSlave = maxCountForSlave | |
| 245 # for comparison purposes, turn this dictionary into a stably-sorted | |
| 246 # list of tuples | |
| 247 self._maxCountForSlaveList = self.maxCountForSlave.items() | |
| 248 self._maxCountForSlaveList.sort() | |
| 249 self._maxCountForSlaveList = tuple(self._maxCountForSlaveList) | |
| OLD | NEW |