Package buildbot :: Module locks
[frames] | no frames]

Source Code for Module buildbot.locks

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Copyright Buildbot Team Members 
 15   
 16   
 17  from twisted.python import log 
 18  from twisted.internet import defer 
 19  from buildbot import util 
 20  from buildbot.util import subscription 
 21  from buildbot.util.eventual import eventually 
 22   
 23  if False: # for debugging 
 24      debuglog = log.msg 
 25  else: 
 26      debuglog = lambda m: None 
 27   
28 -class BaseLock:
29 """ 30 Class handling claiming and releasing of L{self}, and keeping track of 31 current and waiting owners. 32 33 We maintain the wait queue in FIFO order, and ensure that counting waiters 34 in the queue behind exclusive waiters cannot acquire the lock. This ensures 35 that exclusive waiters are not starved. 36 """ 37 description = "<BaseLock>" 38
39 - def __init__(self, name, maxCount=1):
40 self.name = name # Name of the lock 41 self.waiting = [] # Current queue, tuples (waiter, LockAccess, 42 # deferred) 43 self.owners = [] # Current owners, tuples (owner, LockAccess) 44 self.maxCount = maxCount # maximal number of counting owners 45 46 # subscriptions to this lock being released 47 self.release_subs = subscription.SubscriptionPoint("%r releases" 48 % (self,))
49
50 - def __repr__(self):
51 return self.description
52
53 - def _getOwnersCount(self):
54 """ Return the number of current exclusive and counting owners. 55 56 @return: Tuple (number exclusive owners, number counting owners) 57 """ 58 num_excl, num_counting = 0, 0 59 for owner in self.owners: 60 if owner[1].mode == 'exclusive': 61 num_excl = num_excl + 1 62 else: # mode == 'counting' 63 num_counting = num_counting + 1 64 65 assert (num_excl == 1 and num_counting == 0) \ 66 or (num_excl == 0 and num_counting <= self.maxCount) 67 return num_excl, num_counting
68 69
70 - def isAvailable(self, requester, access):
71 """ Return a boolean whether the lock is available for claiming """ 72 debuglog("%s isAvailable(%s, %s): self.owners=%r" 73 % (self, requester, access, self.owners)) 74 num_excl, num_counting = self._getOwnersCount() 75 76 # Find all waiters ahead of the requester in the wait queue 77 for idx, waiter in enumerate(self.waiting): 78 if waiter[0] == requester: 79 w_index = idx 80 break 81 else: 82 w_index = len(self.waiting) 83 ahead = self.waiting[:w_index] 84 85 if access.mode == 'counting': 86 # Wants counting access 87 return num_excl == 0 and num_counting + len(ahead) < self.maxCount \ 88 and all([w[1].mode == 'counting' for w in ahead]) 89 else: 90 # Wants exclusive access 91 return num_excl == 0 and num_counting == 0 and len(ahead) == 0
92
93 - def claim(self, owner, access):
94 """ Claim the lock (lock must be available) """ 95 debuglog("%s claim(%s, %s)" % (self, owner, access.mode)) 96 assert owner is not None 97 assert self.isAvailable(owner, access), "ask for isAvailable() first" 98 99 assert isinstance(access, LockAccess) 100 assert access.mode in ['counting', 'exclusive'] 101 self.waiting = [w for w in self.waiting if w[0] != owner] 102 self.owners.append((owner, access)) 103 debuglog(" %s is claimed '%s'" % (self, access.mode))
104
105 - def subscribeToReleases(self, callback):
106 """Schedule C{callback} to be invoked every time this lock is 107 released. Returns a L{Subscription}.""" 108 return self.release_subs.subscribe(callback)
109
110 - def release(self, owner, access):
111 """ Release the lock """ 112 assert isinstance(access, LockAccess) 113 114 debuglog("%s release(%s, %s)" % (self, owner, access.mode)) 115 entry = (owner, access) 116 if not entry in self.owners: 117 debuglog("%s already released" % self) 118 return 119 self.owners.remove(entry) 120 # who can we wake up? 121 # After an exclusive access, we may need to wake up several waiting. 122 # Break out of the loop when the first waiting client should not be awakened. 123 num_excl, num_counting = self._getOwnersCount() 124 for i, (w_owner, w_access, d) in enumerate(self.waiting): 125 if w_access.mode == 'counting': 126 if num_excl > 0 or num_counting == self.maxCount: 127 break 128 else: 129 num_counting = num_counting + 1 130 else: 131 # w_access.mode == 'exclusive' 132 if num_excl > 0 or num_counting > 0: 133 break 134 else: 135 num_excl = num_excl + 1 136 137 # If the waiter has a deferred, wake it up and clear the deferred 138 # from the wait queue entry to indicate that it has been woken. 139 if d: 140 self.waiting[i] = (w_owner, w_access, None) 141 eventually(d.callback, self) 142 143 # notify any listeners 144 self.release_subs.deliver()
145
146 - def waitUntilMaybeAvailable(self, owner, access):
147 """Fire when the lock *might* be available. The caller will need to 148 check with isAvailable() when the deferred fires. This loose form is 149 used to avoid deadlocks. If we were interested in a stronger form, 150 this would be named 'waitUntilAvailable', and the deferred would fire 151 after the lock had been claimed. 152 """ 153 debuglog("%s waitUntilAvailable(%s)" % (self, owner)) 154 assert isinstance(access, LockAccess) 155 if self.isAvailable(owner, access): 156 return defer.succeed(self) 157 d = defer.Deferred() 158 159 # Are we already in the wait queue? 160 w = [i for i, w in enumerate(self.waiting) if w[0] == owner] 161 if w: 162 self.waiting[w[0]] = (owner, access, d) 163 else: 164 self.waiting.append((owner, access, d)) 165 return d
166
167 - def stopWaitingUntilAvailable(self, owner, access, d):
168 debuglog("%s stopWaitingUntilAvailable(%s)" % (self, owner)) 169 assert isinstance(access, LockAccess) 170 assert (owner, access, d) in self.waiting 171 self.waiting = [w for w in self.waiting if w[0] != owner]
172
173 - def isOwner(self, owner, access):
174 return (owner, access) in self.owners
175 176
177 -class RealMasterLock(BaseLock):
178 - def __init__(self, lockid):
179 BaseLock.__init__(self, lockid.name, lockid.maxCount) 180 self.description = "<MasterLock(%s, %s)>" % (self.name, self.maxCount)
181
182 - def getLock(self, slave):
183 return self
184
185 -class RealSlaveLock:
186 - def __init__(self, lockid):
187 self.name = lockid.name 188 self.maxCount = lockid.maxCount 189 self.maxCountForSlave = lockid.maxCountForSlave 190 self.description = "<SlaveLock(%s, %s, %s)>" % (self.name, 191 self.maxCount, 192 self.maxCountForSlave) 193 self.locks = {}
194
195 - def __repr__(self):
196 return self.description
197
198 - def getLock(self, slave):
199 slavename = slave.slavename 200 if not self.locks.has_key(slavename): 201 maxCount = self.maxCountForSlave.get(slavename, 202 self.maxCount) 203 lock = self.locks[slavename] = BaseLock(self.name, maxCount) 204 desc = "<SlaveLock(%s, %s)[%s] %d>" % (self.name, maxCount, 205 slavename, id(lock)) 206 lock.description = desc 207 self.locks[slavename] = lock 208 return self.locks[slavename]
209 210
211 -class LockAccess(util.ComparableMixin):
212 """ I am an object representing a way to access a lock. 213 214 @param lockid: LockId instance that should be accessed. 215 @type lockid: A MasterLock or SlaveLock instance. 216 217 @param mode: Mode of accessing the lock. 218 @type mode: A string, either 'counting' or 'exclusive'. 219 """ 220 221 compare_attrs = ['lockid', 'mode']
222 - def __init__(self, lockid, mode, _skipChecks=False):
223 self.lockid = lockid 224 self.mode = mode 225 226 if not _skipChecks: 227 # these checks fail with mock < 0.8.0 when lockid is a Mock 228 # TODO: remove this in Buildbot-0.9.0+ 229 assert isinstance(lockid, (MasterLock, SlaveLock)) 230 assert mode in ['counting', 'exclusive']
231 232
233 -class BaseLockId(util.ComparableMixin):
234 """ Abstract base class for LockId classes. 235 236 Sets up the 'access()' function for the LockId's available to the user 237 (MasterLock and SlaveLock classes). 238 Derived classes should add 239 - Comparison with the L{util.ComparableMixin} via the L{compare_attrs} 240 class variable. 241 - Link to the actual lock class should be added with the L{lockClass} 242 class variable. 243 """
244 - def access(self, mode):
245 """ Express how the lock should be accessed """ 246 assert mode in ['counting', 'exclusive'] 247 return LockAccess(self, mode)
248
249 - def defaultAccess(self):
250 """ For buildbot 0.7.7 compability: When user doesn't specify an access 251 mode, this one is chosen. 252 """ 253 return self.access('counting')
254 255 256 257 # master.cfg should only reference the following MasterLock and SlaveLock 258 # classes. They are identifiers that will be turned into real Locks later, 259 # via the BotMaster.getLockByID method. 260
261 -class MasterLock(BaseLockId):
262 """I am a semaphore that limits the number of simultaneous actions. 263 264 Builds and BuildSteps can declare that they wish to claim me as they run. 265 Only a limited number of such builds or steps will be able to run 266 simultaneously. By default this number is one, but my maxCount parameter 267 can be raised to allow two or three or more operations to happen at the 268 same time. 269 270 Use this to protect a resource that is shared among all builders and all 271 slaves, for example to limit the load on a common SVN repository. 272 """ 273 274 compare_attrs = ['name', 'maxCount'] 275 lockClass = RealMasterLock
276 - def __init__(self, name, maxCount=1):
277 self.name = name 278 self.maxCount = maxCount
279
280 -class SlaveLock(BaseLockId):
281 """I am a semaphore that limits simultaneous actions on each buildslave. 282 283 Builds and BuildSteps can declare that they wish to claim me as they run. 284 Only a limited number of such builds or steps will be able to run 285 simultaneously on any given buildslave. By default this number is one, 286 but my maxCount parameter can be raised to allow two or three or more 287 operations to happen on a single buildslave at the same time. 288 289 Use this to protect a resource that is shared among all the builds taking 290 place on each slave, for example to limit CPU or memory load on an 291 underpowered machine. 292 293 Each buildslave will get an independent copy of this semaphore. By 294 default each copy will use the same owner count (set with maxCount), but 295 you can provide maxCountForSlave with a dictionary that maps slavename to 296 owner count, to allow some slaves more parallelism than others. 297 298 """ 299 300 compare_attrs = ['name', 'maxCount', '_maxCountForSlaveList'] 301 lockClass = RealSlaveLock
302 - def __init__(self, name, maxCount=1, maxCountForSlave={}):
303 self.name = name 304 self.maxCount = maxCount 305 self.maxCountForSlave = maxCountForSlave 306 # for comparison purposes, turn this dictionary into a stably-sorted 307 # list of tuples 308 self._maxCountForSlaveList = self.maxCountForSlave.items() 309 self._maxCountForSlaveList.sort() 310 self._maxCountForSlaveList = tuple(self._maxCountForSlaveList)
311