blob: ec274a923c41f11dc4a21aa59852eb9681ddd56e [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Classes of failure types."""
from __future__ import print_function
import collections
import json
import sys
import traceback
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import failure_message_lib
from chromite.lib import metrics
class StepFailure(Exception):
"""StepFailure exceptions indicate that a cbuildbot step failed.
Exceptions that derive from StepFailure should meet the following
criteria:
1) The failure indicates that a cbuildbot step failed.
2) The necessary information to debug the problem has already been
printed in the logs for the stage that failed.
3) __str__() should be brief enough to include in a Commit Queue
failure message.
"""
# The constants.EXCEPTION_CATEGORY_ALL_CATEGORIES values that this exception
# maps to. Subclasses should redefine this class constant to map to a
# different category.
EXCEPTION_CATEGORY = constants.EXCEPTION_CATEGORY_UNKNOWN
def __init__(self, message=''):
"""Constructor.
Args:
message: An error message.
"""
Exception.__init__(self, message)
self.args = (message,)
def __str__(self):
"""Stringify the message."""
return self.message
def EncodeExtraInfo(self):
"""Encode extra_info into a json string, can be overwritten by subclasses"""
def ConvertToStageFailureMessage(self, build_stage_id, stage_name,
stage_prefix_name=None):
"""Convert StepFailure to StageFailureMessage.
Args:
build_stage_id: The id of the build stage.
stage_name: The name (string) of the failed stage.
stage_prefix_name: The prefix name (string) of the failed stage,
default to None.
Returns:
An instance of failure_message_lib.StageFailureMessage.
"""
stage_failure = failure_message_lib.StageFailure(
None, build_stage_id, None, self.__class__.__name__, self.message,
self.EXCEPTION_CATEGORY, self.EncodeExtraInfo(), None, stage_name,
None, None, None, None, None, None, None, None, None, None)
return failure_message_lib.StageFailureMessage(
stage_failure, stage_prefix_name=stage_prefix_name)
# A namedtuple to hold information of an exception.
ExceptInfo = collections.namedtuple(
'ExceptInfo', ['type', 'str', 'traceback'])
def CreateExceptInfo(exception, tb):
"""Creates a list of ExceptInfo objects from |exception| and |tb|.
Creates an ExceptInfo object from |exception| and |tb|. If
|exception| is a CompoundFailure with non-empty list of exc_infos,
simly returns exception.exc_infos. Note that we do not preserve type
of |exception| in this case.
Args:
exception: The exception.
tb: The textual traceback.
Returns:
A list of ExceptInfo objects.
"""
if isinstance(exception, CompoundFailure) and exception.exc_infos:
return exception.exc_infos
return [ExceptInfo(exception.__class__, str(exception), tb)]
class CompoundFailure(StepFailure):
"""An exception that contains a list of ExceptInfo objects."""
def __init__(self, message='', exc_infos=None):
"""Initializes an CompoundFailure instance.
Args:
message: A string describing the failure.
exc_infos: A list of ExceptInfo objects.
"""
self.exc_infos = exc_infos if exc_infos else []
if not message:
# By default, print all stored ExceptInfo objects. This is the
# preferred behavior because we'd always have the full
# tracebacks to debug the failure.
self.message = '\n'.join(['{e.type}: {e.str}\n{e.traceback}'.format(e=ex)
for ex in self.exc_infos])
super(CompoundFailure, self).__init__(message=message)
def ToSummaryString(self):
"""Returns a string with type and string of each ExceptInfo object.
This does not include the textual tracebacks on purpose, so the
message is more readable on the waterfall.
"""
if self.HasEmptyList():
# Fall back to return self.message if list is empty.
return self.message
else:
return '\n'.join(['%s: %s' % (e.type, e.str) for e in self.exc_infos])
def HasEmptyList(self):
"""Returns True if self.exc_infos is empty."""
return not bool(self.exc_infos)
def HasFailureType(self, cls):
"""Returns True if any of the failures matches |cls|."""
return any(issubclass(x.type, cls) for x in self.exc_infos)
def MatchesFailureType(self, cls):
"""Returns True if all failures matches |cls|."""
return (not self.HasEmptyList() and
all(issubclass(x.type, cls) for x in self.exc_infos))
def HasFatalFailure(self, whitelist=None):
"""Determine if there are non-whitlisted failures.
Args:
whitelist: A list of whitelisted exception types.
Returns:
Returns True if any failure is not in |whitelist|.
"""
if not whitelist:
return not self.HasEmptyList()
for ex in self.exc_infos:
if all(not issubclass(ex.type, cls) for cls in whitelist):
return True
return False
def ConvertToStageFailureMessage(self, build_stage_id, stage_name,
stage_prefix_name=None):
"""Convert CompoundFailure to StageFailureMessage.
Args:
build_stage_id: The id of the build stage.
stage_name: The name (string) of the failed stage.
stage_prefix_name: The prefix name (string) of the failed stage,
default to None.
Returns:
An instance of failure_message_lib.StageFailureMessage.
"""
stage_failure = failure_message_lib.StageFailure(
None, build_stage_id, None, self.__class__.__name__, self.message,
self.EXCEPTION_CATEGORY, self.EncodeExtraInfo(), None, stage_name,
None, None, None, None, None, None, None, None, None, None)
compound_failure_message = failure_message_lib.CompoundFailureMessage(
stage_failure, stage_prefix_name=stage_prefix_name)
for exc_class, exc_str, _ in self.exc_infos:
inner_failure = failure_message_lib.StageFailure(
None, build_stage_id, None, exc_class.__name__, exc_str,
_GetExceptionCategory(exc_class), None, None, stage_name,
None, None, None, None, None, None, None, None, None, None)
innner_failure_message = failure_message_lib.StageFailureMessage(
inner_failure, stage_prefix_name=stage_prefix_name)
compound_failure_message.inner_failures.append(innner_failure_message)
return compound_failure_message
class ExitEarlyException(Exception):
"""Exception when a stage finishes and exits early."""
# ExitEarlyException is to simulate sys.exit(0), and SystemExit derives
# from BaseException, so should not catch ExitEarlyException as Exception
# and reset type to re-raise.
EXCEPTIONS_TO_EXCLUDE = (ExitEarlyException,)
class SetFailureType(object):
"""A wrapper to re-raise the exception as the pre-set type."""
def __init__(self, category_exception, source_exception=None,
exclude_exceptions=EXCEPTIONS_TO_EXCLUDE):
"""Initializes the decorator.
Args:
category_exception: The exception type to re-raise as. It must be
a subclass of CompoundFailure.
source_exception: The exception types to re-raise. By default, re-raise
all Exception classes.
exclude_exceptions: Do not set the type of the exception if it's subclass
of one exception in exclude_exceptions. Default to EXCLUSIVE_EXCEPTIONS.
"""
assert issubclass(category_exception, CompoundFailure)
self.category_exception = category_exception
self.source_exception = source_exception
if self.source_exception is None:
self.source_exception = Exception
self.exclude_exceptions = exclude_exceptions
def __call__(self, functor):
"""Returns a wrapped function."""
def wrapped_functor(*args, **kwargs):
try:
return functor(*args, **kwargs)
except self.source_exception:
# Get the information about the original exception.
exc_type, exc_value, _ = sys.exc_info()
exc_traceback = traceback.format_exc()
if self.exclude_exceptions is not None:
for exclude_exception in self.exclude_exceptions:
if issubclass(exc_type, exclude_exception):
raise
if issubclass(exc_type, self.category_exception):
# Do not re-raise if the exception is a subclass of the set
# exception type because it offers more information.
raise
else:
exc_infos = CreateExceptInfo(exc_value, exc_traceback)
raise self.category_exception(exc_infos=exc_infos)
return wrapped_functor
class RetriableStepFailure(StepFailure):
"""This exception is thrown when a step failed, but should be retried."""
# TODO(nxia): Everytime the class name is changed, add the new class name to
# BUILD_SCRIPT_FAILURE_TYPES.
class BuildScriptFailure(StepFailure):
"""This exception is thrown when a build command failed.
It is intended to provide a shorter summary of what command failed,
for usage in failure messages from the Commit Queue, so as to ensure
that developers aren't spammed with giant error messages when common
commands (e.g. build_packages) fail.
"""
EXCEPTION_CATEGORY = constants.EXCEPTION_CATEGORY_BUILD
def __init__(self, exception, shortname):
"""Construct a BuildScriptFailure object.
Args:
exception: A RunCommandError object.
shortname: Short name for the command we're running.
"""
StepFailure.__init__(self)
assert isinstance(exception, cros_build_lib.RunCommandError)
self.exception = exception
self.shortname = shortname
self.args = (exception, shortname)
def __str__(self):
"""Summarize a build command failure briefly."""
result = self.exception.result
if result.returncode:
return '%s failed (code=%s)' % (self.shortname, result.returncode)
else:
return self.exception.msg
def EncodeExtraInfo(self):
"""Encode extra_info into a json string.
Returns:
A json string containing shortname.
"""
extra_info_dict = {
'shortname': self.shortname,
}
return json.dumps(extra_info_dict)
# TODO(nxia): Everytime the class name is changed, add the new class name to
# PACKAGE_BUILD_FAILURE_TYPES
class PackageBuildFailure(BuildScriptFailure):
"""This exception is thrown when packages fail to build."""
def __init__(self, exception, shortname, failed_packages):
"""Construct a PackageBuildFailure object.
Args:
exception: The underlying exception.
shortname: Short name for the command we're running.
failed_packages: List of packages that failed to build.
"""
BuildScriptFailure.__init__(self, exception, shortname)
self.failed_packages = set(failed_packages)
self.args = (exception, shortname, failed_packages)
def __str__(self):
return ('Packages failed in %s: %s'
% (self.shortname, ' '.join(sorted(self.failed_packages))))
def EncodeExtraInfo(self):
"""Encode extra_info into a json string.
Returns:
A json string containing shortname and failed_packages.
"""
extra_info_dict = {
'shortname': self.shortname,
'failed_packages': list(self.failed_packages)
}
return json.dumps(extra_info_dict)
def BuildCompileFailureOutputJson(self):
"""Build proto BuildCompileFailureOutput compatible JSON output.
Returns:
A json string with BuildCompileFailureOutput proto as json.
"""
failures = []
for pkg in self.failed_packages:
failures.append({'rule': 'emerge', 'output_targets': pkg})
wrapper = {'failures': failures}
return json.dumps(wrapper, indent=2)
class InfrastructureFailure(CompoundFailure):
"""Raised if a stage fails due to infrastructure issues."""
EXCEPTION_CATEGORY = constants.EXCEPTION_CATEGORY_INFRA
# Chrome OS Test Lab failures.
class TestLabFailure(InfrastructureFailure):
"""Raised if a stage fails due to hardware lab infrastructure issues."""
EXCEPTION_CATEGORY = constants.EXCEPTION_CATEGORY_LAB
class SuiteTimedOut(TestLabFailure):
"""Raised if a test suite timed out with no test failures."""
class BoardNotAvailable(TestLabFailure):
"""Raised if the board is not available in the lab."""
class SwarmingProxyFailure(TestLabFailure):
"""Raised when error related to swarming proxy occurs."""
# Gerrit-on-Borg failures.
class GoBFailure(InfrastructureFailure):
"""Raised if a stage fails due to Gerrit-on-Borg (GoB) issues."""
class GoBQueryFailure(GoBFailure):
"""Raised if a stage fails due to Gerrit-on-Borg (GoB) query errors."""
class GoBSubmitFailure(GoBFailure):
"""Raised if a stage fails due to Gerrit-on-Borg (GoB) submission errors."""
class GoBFetchFailure(GoBFailure):
"""Raised if a stage fails due to Gerrit-on-Borg (GoB) fetch errors."""
# Google Storage failures.
class GSFailure(InfrastructureFailure):
"""Raised if a stage fails due to Google Storage (GS) issues."""
class GSUploadFailure(GSFailure):
"""Raised if a stage fails due to Google Storage (GS) upload issues."""
class GSDownloadFailure(GSFailure):
"""Raised if a stage fails due to Google Storage (GS) download issues."""
# Builder failures.
class BuilderFailure(InfrastructureFailure):
"""Raised if a stage fails due to builder issues."""
class MasterSlaveVersionMismatchFailure(BuilderFailure):
"""Raised if a slave build has a different full_version than its master."""
# Crash collection service failures.
class CrashCollectionFailure(InfrastructureFailure):
"""Raised if a stage fails due to crash collection services."""
class TestFailure(StepFailure):
"""Raised if a test stage (e.g. VMTest) fails."""
EXCEPTION_CATEGORY = constants.EXCEPTION_CATEGORY_TEST
class TestWarning(StepFailure):
"""Raised if a test stage (e.g. VMTest) returns a warning code."""
def ReportStageFailure(buildstore, build_stage_id, exception,
metrics_fields=None):
"""Reports stage failure to CIDB and Mornach along with inner exceptions.
Args:
buildstore: BuildStore instance to make DB calls with.
build_stage_id: The cidb id for the build stage that failed.
exception: The failure exception to report.
metrics_fields: (Optional) Fields for ts_mon metric.
Returns:
The Integer id of this exception in the failureTable (outer_failure_id if
it's a CompoundFailure).
"""
exception_message = (exception.ToSummaryString()
if isinstance(exception, CompoundFailure)
else str(exception))
extra_info = (exception.EncodeExtraInfo()
if isinstance(exception, StepFailure) else None)
outer_failure_id = _InsertFailureToCIDBAndMornach(
buildstore,
build_stage_id,
type(exception).__name__,
exception_message,
exception_category=_GetExceptionCategory(type(exception)),
extra_info=extra_info,
metrics_fields=metrics_fields)
# This assumes that CompoundFailure can't be nested.
if isinstance(exception, CompoundFailure):
for exc_class, exc_str, _ in exception.exc_infos:
_InsertFailureToCIDBAndMornach(
buildstore,
build_stage_id,
exc_class.__name__,
exc_str,
exception_category=_GetExceptionCategory(exc_class),
outer_failure_id=outer_failure_id,
metrics_fields=metrics_fields)
return outer_failure_id
def _InsertFailureToCIDBAndMornach(
buildstore, build_stage_id, exception_type, exception_message,
exception_category=constants.EXCEPTION_CATEGORY_UNKNOWN,
outer_failure_id=None,
extra_info=None,
metrics_fields=None):
"""Report a single stage failure to CIDB and Mornach if needed.
Args:
buildstore: BuildStore instance to make DB calls.
build_stage_id: The cidb id for the build stage that failed.
exception_type: str name of the exception class.
exception_message: str description of the failure.
exception_category: (Optional) one of
constants.EXCEPTION_CATEGORY_ALL_CATEGORIES,
Default: 'unknown'.
outer_failure_id: (Optional) primary key of outer failure which contains
this failure. Used to store CompoundFailure
relationship.
extra_info: (Optional) extra category-specific string description giving
failure details. Used for programmatic triage.
metrics_fields: (Optional) Fields for ts_mon metric.
Returns:
The Integer id of this exception in the failureTable.
"""
assert buildstore.AreClientsReady()
failure_id = buildstore.InsertFailure(
build_stage_id, exception_type, exception_message,
exception_category=exception_category,
outer_failure_id=outer_failure_id,
extra_info=extra_info)
if (metrics_fields is not None and
exception_category != constants.EXCEPTION_CATEGORY_UNKNOWN):
counter = metrics.Counter(constants.MON_STAGE_FAILURE_COUNT)
metrics_fields['exception_category'] = exception_category
counter.increment(fields=metrics_fields)
return failure_id
def GetStageFailureMessageFromException(stage_name, build_stage_id,
exception, stage_prefix_name=None):
"""Get StageFailureMessage from an exception.
Args:
stage_name: The name (string) of the failed stage.
build_stage_id: The id of the failed build stage.
exception: The BaseException instance to convert to StageFailureMessage.
stage_prefix_name: The prefix name (string) of the failed stage,
default to None.
Returns:
An instance of failure_message_lib.StageFailureMessage.
"""
if isinstance(exception, StepFailure):
return exception.ConvertToStageFailureMessage(
build_stage_id, stage_name, stage_prefix_name=stage_prefix_name)
else:
stage_failure = failure_message_lib.StageFailure(
None, build_stage_id, None, type(exception).__name__, str(exception),
_GetExceptionCategory(type(exception)), None, None, stage_name,
None, None, None, None, None, None, None, None, None, None)
return failure_message_lib.StageFailureMessage(
stage_failure, stage_prefix_name=stage_prefix_name)
def _GetExceptionCategory(exception_class):
# Do not use try/catch. If a subclass of StepFailure does not have a valid
# EXCEPTION_CATEGORY, it is a programming error, not a runtime error.
if issubclass(exception_class, StepFailure):
return exception_class.EXCEPTION_CATEGORY
else:
return constants.EXCEPTION_CATEGORY_UNKNOWN