Report retried builds via sql reporter.

Since we added those to the MQTT reporter, we should also store them in
the SQL database. They are stored in the zuul_build table and can be
identified via the new "final" column which is set to False for those
builds (and True for all others).

The final flag can also be used in the API to specifically filter for
those builds or remove them from the result. By default, no builds are
filtered out.

The buildset API includes these retried builds under a dedicated
'retry_builds' key to not mix them up with the final builds. Thus, the
JSON format is equally to the one the MQTT reporter uses.

For the migration of the SQL database, all existing builds will be set
to final.

We could also provide a filter mechanism via the web UI, but that should
be part of a separate change (e.g. add a checkbox next to the search
filter saying "Show retried builds").

Change-Id: I5960df8a997c7ab81a07b9bd8631c14dbe22b8ab
This commit is contained in:
Benedikt Loeffler 2019-01-28 14:38:15 +01:00 committed by Tobias Henkel
parent 99c6db0895
commit 25ccee91af
No known key found for this signature in database
GPG Key ID: 03750DEC158E5FA2
6 changed files with 195 additions and 32 deletions

View File

@ -0,0 +1,9 @@
---
features:
- |
The builds/ and buildset/ API endpoints now include information about
retried builds. They are called non-final as those are all builds that
were retried at least once and thus weren't visible to the user so far.
The builds/ API can filter those retried builds and you might exclude
them from API result by setting ``final=false`` in the API request.

View File

@ -75,7 +75,7 @@ class TestSQLConnection(ZuulDBTestCase):
build_table = table_prefix + 'zuul_build'
self.assertEqual(16, len(insp.get_columns(buildset_table)))
self.assertEqual(11, len(insp.get_columns(build_table)))
self.assertEqual(12, len(insp.get_columns(build_table)))
def test_sql_tables_created(self):
"Test the tables for storing results are created properly"
@ -226,6 +226,87 @@ class TestSQLConnection(ZuulDBTestCase):
check_results('resultsdb_mysql')
check_results('resultsdb_postgresql')
def test_sql_results_retry_builds(self):
"Test that retry results are entered into an sql table correctly"
# Check the results
def check_results(connection_name):
# Grab the sa tables
tenant = self.scheds.first.sched.abide.tenants.get("tenant-one")
reporter = _get_reporter_from_connection_name(
tenant.layout.pipelines["check"].success_actions,
connection_name
)
with self.connections.connections[
connection_name].engine.connect() as conn:
result = conn.execute(
sa.sql.select([reporter.connection.zuul_buildset_table])
)
buildsets = result.fetchall()
self.assertEqual(1, len(buildsets))
buildset0 = buildsets[0]
self.assertEqual('check', buildset0['pipeline'])
self.assertEqual('org/project', buildset0['project'])
self.assertEqual(1, buildset0['change'])
self.assertEqual('1', buildset0['patchset'])
self.assertEqual('SUCCESS', buildset0['result'])
self.assertEqual('Build succeeded.', buildset0['message'])
self.assertEqual('tenant-one', buildset0['tenant'])
self.assertEqual(
'https://review.example.com/%d' % buildset0['change'],
buildset0['ref_url'])
buildset0_builds = conn.execute(
sa.sql.select(
[reporter.connection.zuul_build_table]
).where(
reporter.connection.zuul_build_table.c.buildset_id ==
buildset0['id']
)
).fetchall()
# Check the retry results
self.assertEqual('project-merge', buildset0_builds[0]['job_name'])
self.assertEqual('SUCCESS', buildset0_builds[0]['result'])
self.assertTrue(buildset0_builds[0]['final'])
self.assertEqual('project-test1', buildset0_builds[1]['job_name'])
self.assertEqual('RETRY', buildset0_builds[1]['result'])
self.assertFalse(buildset0_builds[1]['final'])
self.assertEqual('project-test1', buildset0_builds[2]['job_name'])
self.assertEqual('SUCCESS', buildset0_builds[2]['result'])
self.assertTrue(buildset0_builds[2]['final'])
self.assertEqual('project-test2', buildset0_builds[3]['job_name'])
self.assertEqual('RETRY', buildset0_builds[3]['result'])
self.assertFalse(buildset0_builds[3]['final'])
self.assertEqual('project-test2', buildset0_builds[4]['job_name'])
self.assertEqual('SUCCESS', buildset0_builds[4]['result'])
self.assertTrue(buildset0_builds[4]['final'])
self.executor_server.hold_jobs_in_build = True
# Add a retry result
self.log.debug("Adding retry FakeChange")
A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
# Release the merge job (which is the dependency for the other jobs)
self.executor_server.release('.*-merge')
self.waitUntilSettled()
# Let both test jobs fail on the first run, so they are both run again.
self.builds[0].requeue = True
self.builds[1].requeue = True
self.orderedRelease()
self.waitUntilSettled()
check_results('resultsdb_mysql')
check_results('resultsdb_postgresql')
def test_multiple_sql_connections(self):
"Test putting results in different databases"
# Add a successful result

View File

@ -0,0 +1,43 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""add build final column
Revision ID: 269691d2220e
Revises: e0eda5d09eae
Create Date: 2020-01-03 07:53:15.962739
"""
# revision identifiers, used by Alembic.
revision = '269691d2220e'
down_revision = '16c1dc9054d0'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
BUILD_TABLE = 'zuul_build'
def upgrade(table_prefix=''):
op.add_column(table_prefix + BUILD_TABLE, sa.Column('final', sa.Boolean))
# Set all existing build entries to final (otherwise they will vanish from
# the UI)
new_column = sa.table(table_prefix + BUILD_TABLE, sa.column('final'))
op.execute(new_column.update().values(**{'final': True}))
def downgrade():
raise Exception("Downgrades not supported")

View File

@ -60,7 +60,7 @@ class DatabaseSession(object):
change=None, branch=None, patchset=None, ref=None,
newrev=None, event_id=None, uuid=None, job_name=None,
voting=None, node_name=None, result=None, provides=None,
limit=50, offset=0):
final=None, limit=50, offset=0):
build_table = self.connection.zuul_build_table
buildset_table = self.connection.zuul_buildset_table
@ -102,6 +102,7 @@ class DatabaseSession(object):
q = self.listFilter(q, build_table.c.voting, voting)
q = self.listFilter(q, build_table.c.node_name, node_name)
q = self.listFilter(q, build_table.c.result, result)
q = self.listFilter(q, build_table.c.final, final)
q = self.listFilter(q, provides_table.c.name, provides)
q = q.order_by(build_table.c.id.desc()).\
@ -301,6 +302,7 @@ class SQLConnection(BaseConnection):
log_url = sa.Column(sa.String(255))
node_name = sa.Column(sa.String(255))
error_detail = sa.Column(sa.TEXT())
final = sa.Column(sa.Boolean)
buildset = orm.relationship(BuildSetModel, backref="builds")
def createArtifact(self, *args, **kw):

View File

@ -29,6 +29,43 @@ class SQLReporter(BaseReporter):
name = 'sql'
log = logging.getLogger("zuul.SQLReporter")
def _getBuildData(self, item, job, build):
(result, url) = item.formatJobResult(job, build)
log_url = build.result_data.get('zuul', {}).get('log_url')
if log_url and log_url[-1] != '/':
log_url = log_url + '/'
start = end = None
if build.start_time:
start = datetime.datetime.fromtimestamp(
build.start_time,
tz=datetime.timezone.utc)
if build.end_time:
end = datetime.datetime.fromtimestamp(
build.end_time,
tz=datetime.timezone.utc)
return result, log_url, start, end
def createBuildEntry(self, item, job, db_buildset, build, final=True):
# Ensure end_time is defined
if not build.end_time:
build.end_time = time.time()
result, log_url, start, end = self._getBuildData(item, job, build)
db_build = db_buildset.createBuild(
uuid=build.uuid,
job_name=build.job.name,
result=result,
start_time=start,
end_time=end,
voting=build.job.voting,
log_url=log_url,
node_name=build.node_name,
error_detail=build.error_detail,
final=final,
)
return db_build
def report(self, item):
"""Create an entry into a database."""
log = get_annotated_logger(self.log, item.event)
@ -66,35 +103,16 @@ class SQLReporter(BaseReporter):
# stats about builds. It doesn't understand how to store
# information about the change.
continue
# Ensure end_time is defined
if not build.end_time:
build.end_time = time.time()
(result, url) = item.formatJobResult(job)
log_url = build.result_data.get('zuul', {}).get('log_url')
if log_url and log_url[-1] != '/':
log_url = log_url + '/'
start = end = None
if build.start_time:
start = datetime.datetime.fromtimestamp(
build.start_time,
tz=datetime.timezone.utc)
if build.end_time:
end = datetime.datetime.fromtimestamp(
build.end_time,
tz=datetime.timezone.utc)
db_build = db_buildset.createBuild(
uuid=build.uuid,
job_name=build.job.name,
result=result,
start_time=start,
end_time=end,
voting=build.job.voting,
log_url=log_url,
node_name=build.node_name,
error_detail=build.error_detail,
retry_builds = item.current_build_set.getRetryBuildsForJob(
job.name
)
for retry_build in retry_builds:
self.createBuildEntry(
item, job, db_buildset, retry_build, final=False
)
db_build = self.createBuildEntry(item, job, db_buildset, build)
for provides in job.provides:
db_build.createProvides(name=provides)

View File

@ -825,6 +825,7 @@ class ZuulWebAPI(object):
'log_url': build.log_url,
'node_name': build.node_name,
'error_detail': build.error_detail,
'final': build.final,
'artifacts': [],
'provides': [],
}
@ -876,14 +877,18 @@ class ZuulWebAPI(object):
def builds(self, tenant, project=None, pipeline=None, change=None,
branch=None, patchset=None, ref=None, newrev=None,
uuid=None, job_name=None, voting=None, node_name=None,
result=None, limit=50, skip=0):
result=None, final=None, limit=50, skip=0):
connection = self._get_connection(tenant)
# If final is None, we return all builds, both final and non-final
if final is not None:
final = final.lower() == "true"
builds = connection.getBuilds(
tenant=tenant, project=project, pipeline=pipeline, change=change,
branch=branch, patchset=patchset, ref=ref, newrev=newrev,
uuid=uuid, job_name=job_name, voting=voting, node_name=node_name,
result=result, limit=limit, offset=skip)
result=result, final=final, limit=limit, offset=skip)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
@ -920,8 +925,13 @@ class ZuulWebAPI(object):
}
if builds:
ret['builds'] = []
ret['retry_builds'] = []
for build in builds:
ret['builds'].append(self.buildToDict(build))
# Put all non-final (retry) builds under a different key
if not build.final:
ret['retry_builds'].append(self.buildToDict(build))
else:
ret['builds'].append(self.buildToDict(build))
return ret
@cherrypy.expose