keep a running tally of consecutive number of successful or failed builds
This CL adds a google-storage backed streak counter, which counts the
number of consecutive passed or failed builds for each commit queue
builder. This is done by extending the functionality of GSCounter, and
adding a step to ReportStage which updates the gs-backed counter.
BUG=chromium:288741
TEST=Unit tests pass. cbuildbot --buildbot --debug
Change-Id: Ib069f8559dfb6b756c6f30ee73b929ab9dd0294f
Reviewed-on: https://chromium-review.googlesource.com/175142
Reviewed-by: Aviv Keshet <akeshet@chromium.org>
Tested-by: Aviv Keshet <akeshet@chromium.org>
Commit-Queue: Aviv Keshet <akeshet@chromium.org>
diff --git a/buildbot/cbuildbot_stages.py b/buildbot/cbuildbot_stages.py
index bfb043a..d98a12f 100644
--- a/buildbot/cbuildbot_stages.py
+++ b/buildbot/cbuildbot_stages.py
@@ -349,9 +349,9 @@
for entry in results_lib.Results.Get():
timestr = datetime.timedelta(seconds=math.ceil(entry.time))
if entry.result in results_lib.Results.NON_FAILURE_TYPES:
- status = 'passed'
+ status = constants.FINAL_STATUS_PASSED
else:
- status = 'failed'
+ status = constants.FINAL_STATUS_FAILED
metadata['results'].append({
'name': entry.name,
'status': status,
@@ -3218,6 +3218,39 @@
self._sync_instance = sync_instance
self._completion_instance = completion_instance
+ def _UpdateStreakCounter(self, final_status, counter_name,
+ dry_run=False):
+ """Update the given streak counter based on the final status of build.
+
+ A streak counter counts the number of consecutive passes or failures of
+ a particular builder. Consecutive passes are indicated by a positive value,
+ consecutive failures by a negative value.
+
+ Args:
+ final_status: String indicating final status of build,
+ constants.FINAL_STATUS_PASSED indicating success.
+ counter_name: Name of counter to increment, typically the name of the
+ build config.
+ dry_run: Pretend to update counter only. Default: False.
+
+ Returns:
+ The new value of the streak counter.
+ """
+ gs_ctx = gs.GSContext(dry_run=dry_run)
+ counter_url = os.path.join(constants.MANIFEST_VERSIONS_GS_URL,
+ constants.STREAK_COUNTERS,
+ counter_name)
+ gs_counter = gs.GSCounter(gs_ctx, counter_url)
+
+ if final_status == constants.FINAL_STATUS_PASSED:
+ streak_value = gs_counter.StreakIncrement()
+ else:
+ streak_value = gs_counter.StreakDecrement()
+
+ logging.debug('Streak counter value is %s', streak_value)
+ return streak_value
+
+
def PerformStage(self):
acl = ArchivingStage.GetUploadACL(self._build_config)
archive_urls = {}
@@ -3237,14 +3270,21 @@
# Generate the final metadata before we look at the uploaded list.
if results_lib.Results.BuildSucceededSoFar():
- final_status = 'passed'
+ final_status = constants.FINAL_STATUS_PASSED
else:
- final_status = 'failed'
+ final_status = constants.FINAL_STATUS_FAILED
archive_stage.UploadMetadata(final_status=final_status,
sync_instance=self._sync_instance,
completion_instance=
self._completion_instance)
+ # If this was a Commit Queue build, update the streak counter
+ if (self._sync_instance and
+ isinstance(self._sync_instance, CommitQueueSyncStage)):
+ self._UpdateStreakCounter(final_status=final_status,
+ counter_name=board_config.name,
+ dry_run=archive_stage.debug)
+
# Generate the index page needed for public reading.
uploaded = os.path.join(path, commands.UPLOADED_LIST_FILENAME)
if not os.path.exists(uploaded):
diff --git a/buildbot/cbuildbot_stages_unittest.py b/buildbot/cbuildbot_stages_unittest.py
index 4d6b0ab..2531e51 100755
--- a/buildbot/cbuildbot_stages_unittest.py
+++ b/buildbot/cbuildbot_stages_unittest.py
@@ -1072,11 +1072,11 @@
results_skipped = ('SignerTests',)
for result in json_data['results']:
if result['name'] in results_passed:
- self.assertEquals(result['status'], 'passed')
+ self.assertEquals(result['status'], constants.FINAL_STATUS_PASSED)
elif result['name'] in results_failed:
- self.assertEquals(result['status'], 'failed')
+ self.assertEquals(result['status'], constants.FINAL_STATUS_FAILED)
elif result['name'] in results_skipped:
- self.assertEquals(result['status'], 'passed')
+ self.assertEquals(result['status'], constants.FINAL_STATUS_PASSED)
self.assertTrue('skipped' in result['summary'].lower())
# The buildtools manifest doesn't have any overlays. In this case, we can't
@@ -1659,7 +1659,8 @@
def setUp(self):
for cmd in ((osutils, 'ReadFile'), (osutils, 'WriteFile'),
- (commands, 'UploadArchivedFile'),):
+ (commands, 'UploadArchivedFile'),
+ (stages.ReportStage, '_UpdateStreakCounter')):
self.StartPatcher(mock.patch.object(*cmd, autospec=True))
self.StartPatcher(ArchiveStageMock())
self.cq = CLStatusMock()
diff --git a/buildbot/constants.py b/buildbot/constants.py
index a6ba6f9..a8233d4 100644
--- a/buildbot/constants.py
+++ b/buildbot/constants.py
@@ -30,6 +30,11 @@
SDK_TOOLCHAINS_OUTPUT = 'tmp/toolchain-pkgs'
AUTOTEST_BUILD_PATH = 'usr/local/build/autotest'
+# TODO: Eliminate these or merge with manifest_version.py:STATUS_PASSED
+# crbug.com/318930
+FINAL_STATUS_PASSED = 'passed'
+FINAL_STATUS_FAILED = 'failed'
+
# Re-execution API constants.
# Used by --resume and --bootstrap to decipher which options they
# can pass to the target cbuildbot (since it may not have that
@@ -122,6 +127,8 @@
MANIFEST_VERSIONS_GS_URL = 'gs://chromeos-manifest-versions'
TRASH_BUCKET = 'gs://chromeos-throw-away-bucket'
+STREAK_COUNTERS = 'streak_counters'
+
PATCH_BRANCH = 'patch_branch'
STABLE_EBUILD_BRANCH = 'stabilizing_branch'
MERGE_BRANCH = 'merge_branch'
diff --git a/lib/gs.py b/lib/gs.py
index a9cc938..ffccc5c 100644
--- a/lib/gs.py
+++ b/lib/gs.py
@@ -93,12 +93,21 @@
except GSNoSuchKey:
return 0
- def Increment(self):
- """Atomically increment the counter."""
+ def AtomicCounterOperation(self, default_value, operation):
+ """Atomically set the counter value using |operation|.
+
+ Args:
+ default_value: Default value to use for counter, if counter
+ does not exist.
+ operation: Function that takes the current counter value as a
+ parameter, and returns the new desired value.
+ Returns:
+ The new counter value. None if value could not be set.
+ """
generation, _ = self.ctx.GetGeneration(self.path)
for _ in xrange(self.ctx.retries + 1):
try:
- value = 1 if generation == 0 else self.Get() + 1
+ value = default_value if generation == 0 else operation(self.Get())
self.ctx.Copy('-', self.path, input=str(value), version=generation)
return value
except (GSContextPreconditionFailed, GSNoSuchKey):
@@ -111,6 +120,42 @@
raise
generation = new_generation
+ def Increment(self):
+ """Increment the counter.
+
+ Returns:
+ The new counter value. None if value could not be set.
+ """
+ return self.AtomicCounterOperation(1, lambda x: x + 1)
+
+ def Decrement(self):
+ """Decrement the counter.
+
+ Returns:
+ The new counter value. None if value could not be set."""
+ return self.AtomicCounterOperation(-1, lambda x: x - 1)
+
+ def Reset(self):
+ """Reset the counter to zero.
+
+ Returns:
+ The new counter value. None if value could not be set."""
+ return self.AtomicCounterOperation(0, lambda x: 0)
+
+ def StreakIncrement(self):
+ """Increment the counter if it is positive, otherwise set it to 1.
+
+ Returns:
+ The new counter value. None if value could not be set."""
+ return self.AtomicCounterOperation(1, lambda x: x + 1 if x > 0 else 1)
+
+ def StreakDecrement(self):
+ """Decrement the counter if it is negative, otherwise set it to -1.
+
+ Returns:
+ The new counter value. None if value could not be set."""
+ return self.AtomicCounterOperation(-1, lambda x: x - 1 if x < 0 else -1)
+
class GSContext(object):
"""A class to wrap common google storage operations."""