Skip to content

Commit

Permalink
Fixes for interrupting builds before steps have started.
Browse files Browse the repository at this point in the history
Added methods to BaseLock class to stop waiting on a lock, and to
determine if an object is an owner of the lock.  stopBuild will now
stop the build properly even if it's waiting for a lock.

Moved call to build_status.buildStarted(self) to before the call to
acquireLocks so that the build now shows up in the waterfall and
database.

Removed logging of acquire/releaseLocks if there are no locks
configured.

Add current step display to the per-build web status, which will also
indicate if the build is waiting for a lock.

Fixes: #389
  • Loading branch information
Chris AtLee committed Aug 20, 2010
1 parent 49134cc commit 720c6c8
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 29 deletions.
9 changes: 9 additions & 0 deletions master/buildbot/locks.py
Expand Up @@ -115,6 +115,15 @@ def waitUntilMaybeAvailable(self, owner, access):
self.waiting.append((access, d))
return d

def stopWaitingUntilAvailable(self, owner, access, d):
debuglog("%s stopWaitingUntilAvailable(%s)" % (self, owner))
assert isinstance(access, LockAccess)
assert (access, d) in self.waiting
self.waiting.remove( (access, d) )

def isOwner(self, owner, access):
return (owner, access) in self.owners


class RealMasterLock(BaseLock):
def __init__(self, lockid):
Expand Down
30 changes: 25 additions & 5 deletions master/buildbot/process/base.py
Expand Up @@ -60,6 +60,8 @@ def __init__(self, requests):

self.terminate = False

self._acquiringLock = None

def setBuilder(self, builder):
"""
Set the given builder as our builder.
Expand Down Expand Up @@ -229,26 +231,30 @@ def _release_slave(res, slave, bs):
d.callback(self)
return d

self.build_status.buildStarted(self)
self.acquireLocks().addCallback(self._startBuild_2)
return d

def acquireLocks(self, res=None):
log.msg("acquireLocks(step %s, locks %s)" % (self, self.locks))
self._acquiringLock = None
if not self.locks:
return defer.succeed(None)
if self.stopped:
return defer.succeed(None)
log.msg("acquireLocks(build %s, locks %s)" % (self, self.locks))
for lock, access in self.locks:
if not lock.isAvailable(access):
log.msg("Build %s waiting for lock %s" % (self, lock))
d = lock.waitUntilMaybeAvailable(self, access)
d.addCallback(self.acquireLocks)
self._acquiringLock = (lock, access, d)
return d
# all locks are available, claim them all
for lock, access in self.locks:
lock.claim(self, access)
return defer.succeed(None)

def _startBuild_2(self, res):
self.build_status.buildStarted(self)
self.startNextStep()

def setupBuild(self, expectations):
Expand Down Expand Up @@ -427,7 +433,16 @@ def stopBuild(self, reason="<no reason given>"):
# TODO: include 'reason' in this point event
self.builder.builder_status.addPointEvent(['interrupt'])
self.stopped = True
self.currentStep.interrupt(reason)
if self.currentStep:
self.currentStep.interrupt(reason)

self.result = FAILURE
self.text.append("Interrupted")

if self._acquiringLock:
lock, access, d = self._acquiringLock
lock.stopWaitingUntilAvailable(self, access, d)
d.callback(None)

def allStepsDone(self):
if self.result == FAILURE:
Expand Down Expand Up @@ -476,9 +491,14 @@ def buildFinished(self, text, results):
self.deferred = None

def releaseLocks(self):
log.msg("releaseLocks(%s): %s" % (self, self.locks))
if self.locks:
log.msg("releaseLocks(%s): %s" % (self, self.locks))
for lock, access in self.locks:
lock.release(self, access)
if lock.isOwner(self, access):
lock.release(self, access)
else:
# This should only happen if we've been interrupted
assert self.stopped

# IBuildControl

Expand Down
50 changes: 40 additions & 10 deletions master/buildbot/process/buildstep.py
Expand Up @@ -618,6 +618,9 @@ def __init__(self, **kwargs):
raise TypeError(why)
self._pendingLogObservers = []

self._acquiringLock = None
self.stopped = False

def describe(self, done=False):
return [self.name]

Expand Down Expand Up @@ -713,33 +716,45 @@ def startStep(self, remote):
log.msg("Hey, lock %s is claimed by both a Step (%s) and the"
" parent Build (%s)" % (l, self, self.build))
raise RuntimeError("lock claimed by both Step and Build")

# Set the step's text here so that the stepStarted notification sees
# the correct description
self.step_status.setText(self.describe(False))
self.step_status.stepStarted()

d = self.acquireLocks()
d.addCallback(self._startStep_2)
return self.deferred

def acquireLocks(self, res=None):
log.msg("acquireLocks(step %s, locks %s)" % (self, self.locks))
self._acquiringLock = None
if not self.locks:
return defer.succeed(None)
if self.stopped:
return defer.succeed(None)
log.msg("acquireLocks(step %s, locks %s)" % (self, self.locks))
for lock, access in self.locks:
if not lock.isAvailable(access):
self.step_status.setWaitingForLocks(True)
log.msg("step %s waiting for lock %s" % (self, lock))
d = lock.waitUntilMaybeAvailable(self, access)
d.addCallback(self.acquireLocks)
self._acquiringLock = (lock, access, d)
return d
# all locks are available, claim them all
for lock, access in self.locks:
lock.claim(self, access)
self.step_status.setWaitingForLocks(False)
return defer.succeed(None)

def _startStep_2(self, res):
if self.stopped:
self.finished(FAILURE)
return

if self.progress:
self.progress.start()

# Set the step's text here so that the stepStarted notification sees
# the correct description
self.step_status.setText(self.describe(False))
self.step_status.stepStarted()
try:
skip = None
if isinstance(self.doStepIf, bool):
Expand Down Expand Up @@ -827,12 +842,20 @@ def interrupt(self, reason):
local processing should be skipped, and the Step completed with an
error status. The results text should say something useful like
['step', 'interrupted'] or ['remote', 'lost']"""
pass
self.stopped = True
if self._acquiringLock:
lock, access, d = self._acquiringLock
lock.stopWaitingUntilAvailable(self, access, d)
d.callback(None)

def releaseLocks(self):
log.msg("releaseLocks(%s): %s" % (self, self.locks))
for lock, access in self.locks:
lock.release(self, access)
if lock.isOwner(self, access):
lock.release(self, access)
else:
# This should only happen if we've been interrupted
assert self.stopped

def finished(self, results):
if self.progress:
Expand Down Expand Up @@ -986,6 +1009,7 @@ class LoggingBuildStep(BuildStep):
logfiles = {}

parms = BuildStep.parms + ['logfiles', 'lazylogfiles']
cmd = None

def __init__(self, logfiles={}, lazylogfiles=False, *args, **kwargs):
BuildStep.__init__(self, *args, **kwargs)
Expand Down Expand Up @@ -1061,9 +1085,15 @@ def interrupt(self, reason):
# TODO: consider adding an INTERRUPTED or STOPPED status to use
# instead of FAILURE, might make the text a bit more clear.
# 'reason' can be a Failure, or text
self.addCompleteLog('interrupt', str(reason))
d = self.cmd.interrupt(reason)
return d
BuildStep.interrupt(self, reason)
if self.step_status.isWaitingForLocks():
self.addCompleteLog('interrupt while waiting for locks', str(reason))
else:
self.addCompleteLog('interrupt', str(reason))

if self.cmd:
d = self.cmd.interrupt(reason)
return d

def checkDisconnect(self, f):
f.trap(error.ConnectionLost)
Expand Down
8 changes: 8 additions & 0 deletions master/buildbot/status/builder.py
Expand Up @@ -827,6 +827,8 @@ def __init__(self, parent):
self.finishedWatchers = []
self.statistics = {}

self.waitingForLocks = False

def getName(self):
"""Returns a short string with the name of this step. This string
may have spaces in it."""
Expand Down Expand Up @@ -1046,6 +1048,12 @@ def checkLogfiles(self):
# filter out logs that have been deleted
self.logs = [ l for l in self.logs if l.hasContents() ]

def isWaitingForLocks(self):
return self.waitingForLocks

def setWaitingForLocks(self, waiting):
self.waitingForLocks = waiting

# persistence

def __getstate__(self):
Expand Down
30 changes: 21 additions & 9 deletions master/buildbot/status/web/build.py
Expand Up @@ -33,25 +33,33 @@ def content(self, req, cxt):

cxt['b'] = b
cxt['path_to_builder'] = path_to_builder(req, b.getBuilder())

if not b.isFinished():
step = b.getCurrentStep()
if not step:
cxt['current_step'] = "[waiting for Lock]"
else:
if step.isWaitingForLocks():
cxt['current_step'] = "%s [waiting for Lock]" % step.getName()
else:
cxt['current_step'] = step.getName()
when = b.getETA()
if when is not None:
cxt['when'] = util.formatInterval(when)
cxt['when_time'] = time.strftime("%H:%M:%S",
time.localtime(time.time() + when))
else:

else:
cxt['result_css'] = css_classes[b.getResults()]
if b.getTestResults():
cxt['tests_link'] = req.childLink("tests")

ss = cxt['ss'] = b.getSourceStamp()

if ss.branch is None and ss.revision is None and ss.patch is None and not ss.changes:
cxt['most_recent_rev_build'] = True


got_revision = None
try:
got_revision = b.getProperty("got_revision")
Expand All @@ -76,8 +84,12 @@ def content(self, req, cxt):
(start, end) = s.getTimes()
step['time_to_run'] = util.formatInterval(end - start)
elif s.isStarted():
step['css_class'] = "running"
step['time_to_run'] = "running"
if s.isWaitingForLocks():
step['css_class'] = "waiting"
step['time_to_run'] = "waiting for locks"
else:
step['css_class'] = "running"
step['time_to_run'] = "running"
else:
step['css_class'] = "not_started"
step['time_to_run'] = ""
Expand Down
11 changes: 7 additions & 4 deletions master/buildbot/status/web/builder.py
Expand Up @@ -38,11 +38,14 @@ def builder(self, build, req):
time.localtime(time.time() + when))

step = build.getCurrentStep()
if step:
b['current_step'] = step.getName()
else:
# TODO: is this necessarily the case?
if not step:
b['current_step'] = "[waiting for Lock]"
# TODO: is this necessarily the case?
else:
if step.isWaitingForLocks():
b['current_step'] = "%s [waiting for Lock]" % step.getName()
else:
b['current_step'] = step.getName()

b['stop_url'] = path_to_build(req, build) + '/stop'

Expand Down
2 changes: 1 addition & 1 deletion master/buildbot/status/web/files/default.css
Expand Up @@ -360,7 +360,7 @@ div.BuildWaterfall {
border-color: #ACA0B3;
}

.start,.running,td.building {
.start,.running,.waiting,td.building {
color: #666666;
background-color: #ff6;
border-color: #C5C56D;
Expand Down
2 changes: 2 additions & 0 deletions master/buildbot/status/web/templates/build.html
Expand Up @@ -17,6 +17,8 @@ <h2>Build In Progress:</h2>
{% if when_time %}
<p>ETA: {{ when_time }} [{{ when }}]</p>
{% endif %}

{{ current_step }}

{% if authz.advertiseAction('stopBuild') %}
<h2>Stop Build</h2>
Expand Down

0 comments on commit 720c6c8

Please sign in to comment.