# HG changeset patch # User wbell # Date 1272122172 0 # Node ID de466e590545f96de4f014669d84ba96946e8461 # Parent bfd5fd75d1d664e6375de1ec7c222a00946c09b3 Port of [638], [639], [640] to 0.6.x diff --git a/bitten/htdocs/bitten.css b/bitten/htdocs/bitten.css --- a/bitten/htdocs/bitten.css +++ b/bitten/htdocs/bitten.css @@ -93,9 +93,13 @@ list-style-type: none; margin: .5em 0 0; padding: 0; } #content.build #builds ul.steps li.success, +#content.build #builds ul.steps li.in-progress, #content.build #builds ul.steps li.failed { border: 1px solid; margin: 1px 0; padding: 0 2px 0 12px; } +#content.build #builds ul.steps li.in-progress { + background: #dd9; border-color: #966; color: #993; +} #content.build #builds ul.steps li.success { background: #9d9; border-color: #696; color: #393; } diff --git a/bitten/master.py b/bitten/master.py --- a/bitten/master.py +++ b/bitten/master.py @@ -55,7 +55,7 @@ build_all = BoolOption('bitten', 'build_all', False, doc= """Whether to request builds of older revisions even if a younger revision has already been built.""") - + stabilize_wait = IntOption('bitten', 'stabilize_wait', 0, doc= """The time in seconds to wait for the repository to stabilize before queuing up a new build. This allows time for developers to check in @@ -70,7 +70,7 @@ """The directory on the server in which client log files will be stored.""") quick_status = BoolOption('bitten', 'quick_status', False, doc= - """Whether to show the current build status withing the Trac main + """Whether to show the current build status withing the Trac main navigation bar""") def __init__(self): @@ -151,7 +151,7 @@ self._send_response(req, code, body=message, headers=headers) def _process_build_creation(self, req, slave_token): - queue = BuildQueue(self.env, build_all=self.build_all, + queue = BuildQueue(self.env, build_all=self.build_all, stabilize_wait=self.stabilize_wait, timeout=self.slave_timeout) queue.populate() @@ -224,6 +224,7 @@ self.log.info('Build slave %r initiated build %d', build.slave, build.id) build.started = int(time.time()) + build.last_activity = build.started build.update() for listener in BuildSystem(self.env).listeners: @@ -242,6 +243,14 @@ self.log.info('Build slave %r initiated build %d', build.slave, build.id) + # create the first step, mark it as in-progress. + + recipe = Recipe(xmlio.parse(config.recipe)) + stepname = recipe.__iter__().next().id + + step = self._start_new_step(build, stepname) + step.insert() + self._send_response(req, 200, body, headers={ 'Content-Type': 'application/x-bitten+xml', 'Content-Length': str(len(body)), @@ -258,9 +267,11 @@ self._send_error(req, HTTP_BAD_REQUEST, 'XML parser error') stepname = elem.attr['step'] + # we should have created this step previously; if it hasn't, + # the master and slave are processing steps out of order. step = BuildStep.fetch(self.env, build=build.id, name=stepname) - if step: - self._send_error(req, HTTP_CONFLICT, 'Build step already exists') + if not step: + self._send_error(req, HTTP_CONFLICT, 'Build step has not been created.') recipe = Recipe(xmlio.parse(config.recipe)) index = None @@ -280,14 +291,7 @@ db = self.env.get_db_cnx() - step = BuildStep(self.env, build=build.id, name=stepname) - - # not a great way to determine the start/stop time of the - # step, but it's a server time, which eliminates a bunch - # of skew issues. - now = int(time.time()) - step.started = now - float(elem.attr['duration']) - step.stopped = now + step.stopped = int(time.time()) if elem.attr['status'] == 'failure': self.log.warning('Build %s step %s failed', build.id, stepname) @@ -297,6 +301,9 @@ else: step.status = BuildStep.SUCCESS step.errors += [error.gettext() for error in elem.children('error')] + + # TODO: step.update(db=db) + step.delete(db=db) step.insert(db=db) # Collect log messages from the request body @@ -342,11 +349,12 @@ attachment.insert(filename, fileobj, fileobj.len, db=db) # If this was the last step in the recipe we mark the build as - # completed + # completed otherwise just update last_activity if last_step: self.log.info('Slave %s completed build %d ("%s" as of [%s])', build.slave, build.id, build.config, build.rev) build.stopped = step.stopped + build.last_activity = build.stopped # Determine overall outcome of the build by checking the outcome # of the individual steps against the "onerror" specification of @@ -362,6 +370,20 @@ build.status = Build.SUCCESS build.update(db=db) + else: + build.last_activity = step.stopped + build.update(db=db) + + # start the next step. + for num, recipe_step in enumerate(recipe): + if num == index + 1: + next_step = recipe_step + if next_step is None: + self._send_error(req, HTTP_FORBIDDEN, + 'Unable to find step after ' % stepname) + + step = self._start_new_step(build, next_step.id) + step.insert(db=db) db.commit() @@ -376,3 +398,13 @@ 'Location': req.abs_href.builds( build.id, 'steps', stepname)}) + def _start_new_step(self, build, stepname): + """Creates the in-memory representation for a newly started + step, ready to be persisted to the database. + """ + step = BuildStep(self.env, build=build.id, name=stepname) + step.status = BuildStep.IN_PROGRESS + step.started = int(time.time()) + step.stopped = 0 + + return step diff --git a/bitten/model.py b/bitten/model.py --- a/bitten/model.py +++ b/bitten/model.py @@ -373,6 +373,7 @@ Column('rev_time', type='int'), Column('platform', type='int'), Column('slave'), Column('started', type='int'), Column('stopped', type='int'), Column('status', size=1), + Column('last_activity', type='int'), Index(['config', 'rev', 'platform'], unique=True) ], Table('bitten_slave', key=('build', 'propname'))[ @@ -397,7 +398,8 @@ TOKEN = 'token' def __init__(self, env, config=None, rev=None, platform=None, slave=None, - started=0, stopped=0, rev_time=0, status=PENDING): + started=0, stopped=0, last_activity=0, + rev_time=0, status=PENDING): """Initialize a new build with the specified attributes. To actually create this build in the database, the `insert` method needs @@ -411,6 +413,7 @@ self.slave = slave self.started = started or 0 self.stopped = stopped or 0 + self.last_activity = last_activity or 0 self.rev_time = rev_time self.status = status self.slave_info = {} @@ -466,11 +469,12 @@ cursor = db.cursor() cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,platform," - "slave,started,stopped,status) " - "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)", + "slave,started,stopped,last_activity,status) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)", (self.config, self.rev, int(self.rev_time), self.platform, self.slave or '', self.started or 0, - self.stopped or 0, self.status)) + self.stopped or 0, self.last_activity or 0, + self.status)) self.id = db.get_last_id(cursor, 'bitten_build') if self.slave_info: cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)", @@ -497,9 +501,10 @@ cursor = db.cursor() cursor.execute("UPDATE bitten_build SET slave=%s,started=%s," - "stopped=%s,status=%s WHERE id=%s", + "stopped=%s,last_activity=%s,status=%s WHERE id=%s", (self.slave or '', self.started or 0, - self.stopped or 0, self.status, self.id)) + self.stopped or 0, self.last_activity or 0, + self.status, self.id)) cursor.execute("DELETE FROM bitten_slave WHERE build=%s", (self.id,)) if self.slave_info: cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)", @@ -515,7 +520,8 @@ cursor = db.cursor() cursor.execute("SELECT config,rev,rev_time,platform,slave,started," - "stopped,status FROM bitten_build WHERE id=%s", (id,)) + "stopped,last_activity,status FROM bitten_build WHERE " + "id=%s", (id,)) row = cursor.fetchone() if not row: return None @@ -523,7 +529,9 @@ build = Build(env, config=row[0], rev=row[1], rev_time=int(row[2]), platform=int(row[3]), slave=row[4], started=row[5] and int(row[5]) or 0, - stopped=row[6] and int(row[6]) or 0, status=row[7]) + stopped=row[6] and int(row[6]) or 0, + last_activity=row[7] and int(row[7]) or 0, + status=row[8]) build.id = int(id) cursor.execute("SELECT propname,propvalue FROM bitten_slave " "WHERE build=%s", (id,)) @@ -587,6 +595,7 @@ # Step status codes SUCCESS = 'S' + IN_PROGRESS = 'I' FAILURE = 'F' def __init__(self, env, build=None, name=None, description=None, @@ -610,7 +619,8 @@ doc='Whether this build step exists in the database') successful = property(fget=lambda self: self.status == BuildStep.SUCCESS, doc='Whether the build step was successful') - + completed = property(fget=lambda self: self.status == BuildStep.SUCCESS or self.status == BuildStep.FAILURE, + doc='Whether this build step has completed processing') def delete(self, db=None): """Remove the build step from the database.""" if not db: @@ -645,7 +655,7 @@ handle_ta = False assert self.build and self.name - assert self.status in (self.SUCCESS, self.FAILURE) + assert self.status in (self.SUCCESS, self.IN_PROGRESS, self.FAILURE) cursor = db.cursor() cursor.execute("INSERT INTO bitten_step (build,name,description,status," @@ -694,7 +704,7 @@ if not db: db = env.get_db_cnx() - assert status in (None, BuildStep.SUCCESS, BuildStep.FAILURE) + assert status in (None, BuildStep.SUCCESS, BuildStep.IN_PROGRESS, BuildStep.FAILURE) where_clauses = [] if build is not None: @@ -709,7 +719,7 @@ where = "" cursor = db.cursor() - cursor.execute("SELECT build,name FROM bitten_step %s ORDER BY stopped" + cursor.execute("SELECT build,name FROM bitten_step %s ORDER BY started" % where, [wc[1] for wc in where_clauses]) for build, name in cursor: yield BuildStep.fetch(env, build, name, db=db) @@ -1031,4 +1041,4 @@ schema = BuildConfig._schema + TargetPlatform._schema + Build._schema + \ BuildStep._schema + BuildLog._schema + Report._schema -schema_version = 11 +schema_version = 12 diff --git a/bitten/queue.py b/bitten/queue.py --- a/bitten/queue.py +++ b/bitten/queue.py @@ -24,6 +24,7 @@ import time from trac.util.datefmt import to_timestamp +from trac.util import pretty_timedelta, format_datetime from trac.attachment import Attachment @@ -276,16 +277,21 @@ db = self.env.get_db_cnx() now = int(time.time()) for build in Build.select(self.env, status=Build.IN_PROGRESS, db=db): - if now - build.started < self.timeout: + if now - build.last_activity < self.timeout: # This build has not reached the timeout yet, assume it's still # being executed - # FIXME: ideally, we'd base this check on the last activity on - # the build, not the start time continue + + self.log.info('Orphaning build %d. Last activity was %s (%s)' % \ + (build.id, format_datetime(build.last_activity), + pretty_timedelta(build.last_activity))) + build.status = Build.PENDING build.slave = None build.slave_info = {} build.started = 0 + build.stopped = 0 + build.last_activity = 0 for step in list(BuildStep.select(self.env, build=build.id, db=db)): step.delete(db=db) build.update(db=db) diff --git a/bitten/templates/bitten_config.html b/bitten/templates/bitten_config.html --- a/bitten/templates/bitten_config.html +++ b/bitten/templates/bitten_config.html @@ -18,6 +18,17 @@ +
+ + +
Duration: ${build.duration}
+
+ +
${build.started} (${build.started_delta} ago)
+
+
+
+
$slave.name ($slave.ipnr)
$slave.os_name $slave.os_version @@ -27,12 +38,12 @@
$config.builds_inprogress in-progress - builds ( - + builds ( $platform.name: $platform.builds_inprogress )
@@ -137,9 +147,8 @@ $platform.name: $platform.builds_pending ) -
$config.builds_inprogress in-progress builds ( - - $platform.name: $platform.builds_inprogress +
$config.builds_inprogress in-progress builds ( + $platform.name: $platform.builds_inprogress )
@@ -173,6 +182,7 @@ ${build_status(build.status)} ${slave_info(build.slave)} + ${build_time(build)}
${build_steps(build.steps)} @@ -201,6 +211,7 @@ $build.id: $build.platform ${slave_info(build.slave)} + ${build_time(build)}
${build_steps(build.steps)} diff --git a/bitten/tests/master.py b/bitten/tests/master.py --- a/bitten/tests/master.py +++ b/bitten/tests/master.py @@ -261,7 +261,7 @@ def test_initiate_build(self): config = BuildConfig(self.env, 'test', path='somepath', active=True, - recipe='') + recipe='') config.insert() platform = TargetPlatform(self.env, config='test', name="Unix") platform.rules.append(('family', 'posix')) @@ -288,14 +288,14 @@ self.assertRaises(RequestDone, module.process_request, req) self.assertEqual(200, outheaders['Status']) - self.assertEqual('90', outheaders['Content-Length']) + self.assertEqual('112', outheaders['Content-Length']) self.assertEqual('application/x-bitten+xml', outheaders['Content-Type']) self.assertEqual('attachment; filename=recipe_test_r123.xml', outheaders['Content-Disposition']) self.assertEqual('', + ' revision="123">', outbody.getvalue()) # Make sure the started timestamp has been set @@ -376,6 +376,9 @@ write=outbody.write, incookie=Cookie('trac_auth=123')) module = BuildMaster(self.env) + + module._start_new_step(build, 'foo').insert() + assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) @@ -428,6 +431,9 @@ write=outbody.write, incookie=Cookie('trac_auth=123')) module = BuildMaster(self.env) + + module._start_new_step(build, 'foo').insert() + assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) @@ -490,6 +496,9 @@ write=outbody.write, incookie=Cookie('trac_auth=123')) module = BuildMaster(self.env) + + module._start_new_step(build, 'foo').insert() + assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) @@ -563,6 +572,9 @@ write=outbody.write, incookie=Cookie('trac_auth=123')) module = BuildMaster(self.env) + + module._start_new_step(build, 'foo').insert() + assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) @@ -633,6 +645,9 @@ write=outbody.write, incookie=Cookie('trac_auth=')) module = BuildMaster(self.env) + + module._start_new_step(build, 'foo').insert() + assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) @@ -646,7 +661,7 @@ assert not build.stopped steps = list(BuildStep.select(self.env, build.id)) - self.assertEqual(0, len(steps)) + self.assertEqual(1, len(steps)) def test_process_build_step_invalidated_build(self): recipe = """ @@ -683,6 +698,9 @@ write=outbody.write, incookie=Cookie('trac_auth=123')) module = BuildMaster(self.env) + + module._start_new_step(build, 'foo').insert() + assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) @@ -692,7 +710,7 @@ assert not build.stopped steps = list(BuildStep.select(self.env, build.id)) - self.assertEqual(1, len(steps)) + self.assertEqual(2, len(steps)) # invalidate the build. @@ -723,6 +741,9 @@ write=outbody.write, incookie=Cookie('trac_auth=123')) module = BuildMaster(self.env) + + module._start_new_step(build, 'foo').insert() + assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) @@ -763,6 +784,9 @@ write=outbody.write, incookie=Cookie('trac_auth=123')) module = BuildMaster(self.env) + + module._start_new_step(build, 'foo').insert() + assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) @@ -812,6 +836,9 @@ write=outbody.write, incookie=Cookie('trac_auth=123')) module = BuildMaster(self.env) + + module._start_new_step(build, 'foo').insert() + assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) @@ -906,6 +933,9 @@ incookie=Cookie('trac_auth=')) module = BuildMaster(self.env) + + module._start_new_step(build, 'foo').insert() + assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) @@ -933,6 +963,9 @@ incookie=Cookie('trac_auth=')) module = BuildMaster(self.env) + + module._start_new_step(build, 'foo').insert() + assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) diff --git a/bitten/tests/queue.py b/bitten/tests/queue.py --- a/bitten/tests/queue.py +++ b/bitten/tests/queue.py @@ -392,12 +392,12 @@ platform.insert() build1 = Build(self.env, config='test', platform=platform.id, rev=123, rev_time=42, status=Build.IN_PROGRESS, slave='heinz', - started=time.time() - 600) # Started ten minutes ago + last_activity=time.time() - 600) # active ten minutes ago build1.insert() build2 = Build(self.env, config='test', platform=platform.id, rev=124, rev_time=42, status=Build.IN_PROGRESS, slave='heinz', - started=time.time() - 60) # Started a minute ago + last_activity=time.time() - 60) # active a minute ago build2.insert() queue = BuildQueue(self.env, timeout=300) # 5 minutes timeout diff --git a/bitten/tests/upgrades.py b/bitten/tests/upgrades.py --- a/bitten/tests/upgrades.py +++ b/bitten/tests/upgrades.py @@ -170,6 +170,7 @@ 'old_log_v5', 'old_log_v8', 'old_rule', + 'old_build_v11', ] basic_data = [ diff --git a/bitten/upgrades.py b/bitten/upgrades.py --- a/bitten/upgrades.py +++ b/bitten/upgrades.py @@ -125,6 +125,34 @@ "max_rev,label,description) SELECT name,path,0,'',NULL," "NULL,label,description FROM old_config") +def add_last_activity_to_build(env, db): + """Add a column for storing the last activity to the build table.""" + from trac.db import Table, Column, Index + cursor = db.cursor() + + build_table_schema_v12 = Table('bitten_build', key='id')[ + Column('id', auto_increment=True), Column('config'), Column('rev'), + Column('rev_time', type='int'), Column('platform', type='int'), + Column('slave'), Column('started', type='int'), + Column('stopped', type='int'), Column('status', size=1), + Column('last_activity', type='int'), + Index(['config', 'rev', 'platform'], unique=True) + ] + + cursor.execute("CREATE TEMPORARY TABLE old_build_v11 AS " + "SELECT * FROM bitten_build") + cursor.execute("DROP TABLE bitten_build") + + connector, _ = DatabaseManager(env)._get_connector() + for stmt in connector.to_sql(build_table_schema_v12): + cursor.execute(stmt) + + # it's safe to make the last activity the stop time of the build + cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,platform," + "slave,started,stopped,last_activity,status) " + "SELECT config,rev,rev_time,platform," + "slave,started,stopped,stopped,status FROM old_build_v11") + def add_config_to_reports(env, db): """Add the name of the build configuration as metadata to report documents stored in the BDB XML database.""" @@ -199,7 +227,7 @@ def xmldb_to_db(env, db): """Migrate report data from Berkeley DB XML to SQL database. - + Depending on the number of reports stored, this might take rather long. After the upgrade is done, the bitten.dbxml file (and any BDB XML log files) may be deleted. BDB XML is no longer used by Bitten. @@ -562,4 +590,5 @@ 9: [recreate_rule_with_int_id], 10: [add_config_platform_rev_index_to_build, fix_sequences], 11: [fix_log_levels_misnaming, remove_stray_log_levels_files], + 12: [add_last_activity_to_build], } diff --git a/bitten/web_ui.py b/bitten/web_ui.py --- a/bitten/web_ui.py +++ b/bitten/web_ui.py @@ -12,6 +12,7 @@ import posixpath import re +import time from StringIO import StringIO from datetime import datetime @@ -45,6 +46,9 @@ Build.IN_PROGRESS: 'In Progress', Build.SUCCESS: 'Success', Build.FAILURE: 'Failure'} +_step_status_label = {BuildStep.SUCCESS: 'success', + BuildStep.FAILURE: 'failed', + BuildStep.IN_PROGRESS: 'in progress'} def _get_build_data(env, req, build): data = {'id': build.id, 'name': build.slave, 'rev': build.rev, @@ -89,7 +93,7 @@ status = '' if BuildMaster(self.env).quick_status: repos = self.env.get_repository(req.authname) - for config in BuildConfig.select(self.env, + for config in BuildConfig.select(self.env, include_inactive=False): prev_rev = None for platform, rev, build in collect_changes(repos, config): @@ -106,11 +110,11 @@ status='bitteninprogress' elif not status: if (build_data['status'] == 'completed'): - status='bittencompleted' + status='bittencompleted' if not status: status='bittenpending' yield ('mainnav', 'build', - tag.a('Build Status', href=req.href.build(), accesskey=5, + tag.a('Build Status', href=req.href.build(), accesskey=5, class_=status)) # ITemplatesProvider methods @@ -164,16 +168,16 @@ return 'bitten_config.html', data, None # IRequestHandler methods - + def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if template: add_stylesheet(req, 'bitten/bitten.css') - + return template, data, content_type - + # Internal methods def _render_overview(self, req): @@ -301,9 +305,10 @@ build_data['steps'].append({ 'name': step.name, 'description': step.description, - 'duration': to_datetime(step.stopped, utc) - \ + 'duration': to_datetime(step.stopped or int(time.time()), utc) - \ to_datetime(step.started, utc), - 'failed': not step.successful, + 'status': _step_status_label[step.status], + 'cls': _step_status_label[step.status].replace(' ', '-'), 'errors': step.errors, 'href': build_data['href'] + '#step_' + step.name }) @@ -311,7 +316,7 @@ builds.append(build_data) current_builds += 1 - if current_builds == 0: + if current_builds == 0: continue description = config.description @@ -325,7 +330,7 @@ 'builds': builds }) - data['configs'] = configs + data['configs'] = sorted(configs, key=lambda x:x['label'].lower()) return data def _render_config(self, req, config_name): @@ -372,7 +377,7 @@ db=db)) data['config']['platforms'] = [ { 'name': platform.name, - 'id': platform.id, + 'id': platform.id, 'builds_pending': len(list(Build.select(self.env, config=config.name, status=Build.PENDING, @@ -393,13 +398,13 @@ if has_reports: chart_generators = [] report_categories = list(self._report_categories_for_config(config)) - for generator in ReportChartController(self.env).generators: - for category in generator.get_supported_categories(): + for generator in ReportChartController(self.env).generators: + for category in generator.get_supported_categories(): if category in report_categories: chart_generators.append({ - 'href': req.href.build(config.name, 'chart/' + category) + 'href': req.href.build(config.name, 'chart/' + category) }) - data['config']['charts'] = chart_generators + data['config']['charts'] = chart_generators charts_license = self.config.get('bitten', 'charts_license') if charts_license: data['config']['charts_license'] = charts_license @@ -428,9 +433,11 @@ build_data['steps'].append({ 'name': step.name, 'description': step.description, - 'duration': to_datetime(step.stopped, utc) - \ + 'duration': to_datetime(step.stopped or int(time.time()), utc) - \ to_datetime(step.started, utc), - 'failed': not step.successful, + 'status': _step_status_label[step.status], + 'cls': _step_status_label[step.status].replace(' ', '-'), + 'errors': step.errors, 'href': build_data['href'] + '#step_' + step.name }) @@ -457,16 +464,16 @@ """Yields the categories of reports that exist for active builds of this configuration. """ - + db = self.env.get_db_cnx() repos = self.env.get_repository() cursor = db.cursor() - + cursor.execute("""SELECT DISTINCT report.category as category -FROM bitten_build AS build +FROM bitten_build AS build JOIN bitten_report AS report ON (report.build=build.id) -WHERE build.config=%s AND build.rev_time >= %s AND build.rev_time <= %s""", - (config.name, +WHERE build.config=%s AND build.rev_time >= %s AND build.rev_time <= %s""", + (config.name, config.min_rev_time(self.env), config.max_rev_time(self.env))) @@ -545,8 +552,9 @@ for step in BuildStep.select(self.env, build=build.id, db=db): steps.append({ 'name': step.name, 'description': step.description, - 'duration': pretty_timedelta(step.started, step.stopped), - 'failed': step.status == BuildStep.FAILURE, + 'duration': pretty_timedelta(step.started, step.stopped or int(time.time())), + 'status': _step_status_label[step.status], + 'cls': _step_status_label[step.status].replace(' ', '-'), 'errors': step.errors, 'log': self._render_log(req, build, formatters, step), 'reports': self._render_reports(req, config, build, summarizers, @@ -668,7 +676,9 @@ step.delete(db=db) build.slave = None - build.started = build.stopped = 0 + build.started = 0 + build.stopped = 0 + build.last_activity = 0 build.status = Build.PENDING build.slave_info = {} build.update()