Skip to content

Commit 6699ba4

Browse files
MDBF-1200: Implement OldBuildCanceller
Make tarball-docker cancel older queued or running build requests for the same branch. This ensures that builds for older commits on the same branch are cancelled, so only one commit per branch is built at a time.
1 parent de010bf commit 6699ba4

2 files changed

Lines changed: 243 additions & 3 deletions

File tree

master-protected-branches/master.cfg

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ from common_factories import getLastNFailedBuildsFactory, getQuickBuildFactory
1010
from locks import getLocks
1111
from master_common import base_master_config
1212
from utils import (
13+
CancelOlderSameBranchRequests,
1314
canStartBuild,
1415
createWorker,
1516
isJepsenBranch,
@@ -28,7 +29,7 @@ cfg_dir = os.path.abspath(os.path.dirname(__file__))
2829
#     └── master.cfg
2930
#
3031
# Non autogen masters load from <srcdir> for now.
31-
base_dir = os.path.abspath(f'{cfg_dir}/../')
32+
base_dir = os.path.abspath(f"{cfg_dir}/../")
3233

3334
# Load the slave, database passwords and 3rd-party tokens from an external private file, so
3435
# that the rest of the configuration can be public.
@@ -198,6 +199,16 @@ for w_name in ["aarch64-bbw"]:
198199

199200
## f_tarball - create source tarball
200201
f_tarball = util.BuildFactory()
202+
203+
f_tarball.addStep(
204+
CancelOlderSameBranchRequests(
205+
dry_run=True,
206+
same_builder_only=False,
207+
cancel_claimed=True,
208+
buildbot_base_url=os.environ.get("BUILDMASTER_URL"),
209+
)
210+
)
211+
201212
f_tarball.addStep(
202213
steps.ShellCommand(command=["echo", " revision: ", util.Property("revision")])
203214
)

utils.py

Lines changed: 231 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
from pyzabbix import ZabbixAPI
99
from twisted.internet import defer, threads
1010
from twisted.python import log
11-
1211
from buildbot.buildrequest import BuildRequest
12+
from buildbot.data.resultspec import Filter
1313
from buildbot.interfaces import IProperties
1414
from buildbot.master import BuildMaster
1515
from buildbot.plugins import steps, util, worker
1616
from buildbot.process.builder import Builder
1717
from buildbot.process.buildstep import BuildStep
18-
from buildbot.process.results import FAILURE
18+
from buildbot.process.results import FAILURE, SUCCESS
1919
from buildbot.process.workerforbuilder import AbstractWorkerForBuilder
2020
from buildbot.worker import AbstractWorker
2121
from constants import (
@@ -678,3 +678,232 @@ def mtrEnv(props: IProperties) -> dict:
678678
mtr_add_env[key] = value
679679
return mtr_add_env
680680
return MTR_ENV
681+
682+
# TODO: Upgrading buildbot to 4.* deprecates this class
683+
# Use instead the OldBuildCanceller service
684+
# https://docs.buildbot.net/latest/manual/configuration/services/old_build_canceller.html
685+
class CancelOlderSameBranchRequests(BuildStep):
686+
name = "cancel older obsolete buildrequests"
687+
description = ["checking older matching requests"]
688+
descriptionDone = ["older matching requests checked"]
689+
690+
def __init__(
691+
self,
692+
dry_run=False,
693+
same_builder_only=False,
694+
cancel_claimed=True,
695+
buildbot_base_url=None,
696+
**kwargs,
697+
):
698+
super().__init__(**kwargs)
699+
self.dry_run = dry_run
700+
self.same_builder_only = same_builder_only
701+
self.cancel_claimed = cancel_claimed
702+
self.buildbot_base_url = (
703+
buildbot_base_url.rstrip("/") if buildbot_base_url else None
704+
)
705+
self._builder_name_cache = {}
706+
707+
def _buildrequest_url(self, brid):
708+
if not self.buildbot_base_url:
709+
return None
710+
return f"{self.buildbot_base_url}/#/buildrequests/{brid}"
711+
712+
@defer.inlineCallbacks
713+
def _builder_name(self, builderid):
714+
if builderid in self._builder_name_cache:
715+
return self._builder_name_cache[builderid]
716+
717+
builder = yield self.master.data.get(("builders", builderid))
718+
name = builder.get("name", f"<builderid={builderid}>")
719+
self._builder_name_cache[builderid] = name
720+
return name
721+
722+
@staticmethod
723+
def _fmt_ss(ss):
724+
return (
725+
f"branch={ss.get('branch')!r}, "
726+
f"repository={ss.get('repository')!r}, "
727+
f"revision={ss.get('revision')!r}, "
728+
f"codebase={ss.get('codebase', '')!r}"
729+
)
730+
731+
@defer.inlineCallbacks
732+
def run(self):
733+
current_buildid = self.build.buildid
734+
735+
current_build = yield self.master.data.get(("builds", current_buildid))
736+
current_buildrequestid = current_build["buildrequestid"]
737+
current_builderid = current_build["builderid"]
738+
current_buildername = yield self._builder_name(current_builderid)
739+
740+
current_buildrequest = yield self.master.data.get(
741+
("buildrequests", current_buildrequestid)
742+
)
743+
current_buildsetid = current_buildrequest["buildsetid"]
744+
745+
current_buildset = yield self.master.data.get(("buildsets", current_buildsetid))
746+
current_submitted_at = current_buildset.get("submitted_at")
747+
current_sourcestamps = current_buildset.get("sourcestamps", [])
748+
749+
if current_submitted_at is None or not current_sourcestamps:
750+
self.addCompleteLog(
751+
"summary",
752+
"Current buildset is missing submitted_at or sourcestamps; nothing to do.\n",
753+
)
754+
return SUCCESS
755+
756+
# We want only running or in queue buildrequests
757+
filters = [Filter("complete", "eq", [False])]
758+
# Narrow the search to cancel only buildrequests for the calling builder
759+
if self.same_builder_only:
760+
filters.append(Filter("builderid", "eq", [current_builderid]))
761+
762+
# Getting all buildrequests based on filters
763+
buildrequests = yield self.master.data.get(
764+
("buildrequests",),
765+
filters=filters,
766+
fields=[
767+
"buildrequestid",
768+
"buildsetid",
769+
"builderid",
770+
"claimed",
771+
"complete",
772+
"submitted_at",
773+
],
774+
)
775+
776+
# Log info about the current build
777+
lines = []
778+
lines.append(f"Mode: {'DRY-RUN' if self.dry_run else 'ACTIVE'}")
779+
lines.append(f"same_builder_only={self.same_builder_only}")
780+
lines.append(f"cancel_claimed={self.cancel_claimed}")
781+
lines.append("")
782+
lines.append("Current:")
783+
lines.append(f" buildid={current_buildid}")
784+
lines.append(f" buildrequestid={current_buildrequestid}")
785+
lines.append(f" builderid={current_builderid}")
786+
lines.append(f" buildername={current_buildername!r}")
787+
lines.append(f" buildsetid={current_buildsetid}")
788+
lines.append(f" submitted_at={current_submitted_at}")
789+
current_url = self._buildrequest_url(current_buildrequestid)
790+
if current_url:
791+
lines.append(f" url={current_url}")
792+
lines.append(" sourcestamps:")
793+
for i, ss in enumerate(current_sourcestamps, 1):
794+
lines.append(f" [{i}] {self._fmt_ss(ss)}")
795+
lines.append("")
796+
797+
matches = []
798+
actions = []
799+
800+
for br in buildrequests:
801+
brid = br["buildrequestid"]
802+
803+
# Skip self
804+
if brid == current_buildrequestid:
805+
continue
806+
807+
# Skip cancelling running builds if cancel_claimed is False
808+
if not self.cancel_claimed and br.get("claimed"):
809+
continue
810+
811+
other_buildsetid = br["buildsetid"]
812+
other_buildset = yield self.master.data.get(("buildsets", other_buildsetid))
813+
other_submitted_at = other_buildset.get("submitted_at")
814+
other_sourcestamps = other_buildset.get("sourcestamps", [])
815+
816+
if other_submitted_at is None:
817+
continue
818+
819+
# Newest wins: only cancel OLDER matching requests
820+
if other_submitted_at >= current_submitted_at:
821+
continue
822+
823+
# A match means same branch+repository+codebase but different revision
824+
matched_other_ss = None
825+
826+
# If the buildset can have multiple sourcestamps
827+
for current_ss in current_sourcestamps:
828+
for other_ss in other_sourcestamps:
829+
same_target = (
830+
other_ss.get("codebase", "") == current_ss.get("codebase", "")
831+
and other_ss.get("repository") == current_ss.get("repository")
832+
and other_ss.get("branch") == current_ss.get("branch")
833+
)
834+
different_revision = other_ss.get("revision") != current_ss.get(
835+
"revision"
836+
)
837+
838+
if same_target and different_revision:
839+
matched_other_ss = other_ss
840+
break
841+
if matched_other_ss is not None:
842+
break
843+
844+
if matched_other_ss is None:
845+
continue
846+
847+
other_builderid = br["builderid"]
848+
other_buildername = yield self._builder_name(other_builderid)
849+
850+
info = {
851+
"buildrequestid": brid,
852+
"buildername": other_buildername,
853+
"claimed": br.get("claimed"),
854+
"complete": br.get("complete"),
855+
"submitted_at": other_submitted_at,
856+
"branch": matched_other_ss.get("branch"),
857+
"repository": matched_other_ss.get("repository"),
858+
"revision": matched_other_ss.get("revision"),
859+
"codebase": matched_other_ss.get("codebase", ""),
860+
"url": self._buildrequest_url(brid),
861+
}
862+
matches.append(info)
863+
864+
action_prefix = "[DRY-RUN] would cancel" if self.dry_run else "Canceled"
865+
msg = (
866+
f"{action_prefix} buildrequest {brid} "
867+
f"(buildername={other_buildername!r}, "
868+
f"claimed={br.get('claimed')}, "
869+
f"submitted_at={other_submitted_at}, "
870+
f"revision={matched_other_ss.get('revision')!r})"
871+
)
872+
if info["url"]:
873+
msg += f" url={info['url']}"
874+
actions.append(msg)
875+
876+
# Dry-run mode doesn't actually cancel, just log what would be cancelled
877+
if not self.dry_run:
878+
yield self.master.data.control(
879+
"cancel",
880+
{"reason": ("Superseded by newer build for same branch")},
881+
("buildrequests", brid),
882+
)
883+
884+
lines.append(f"Matched older buildrequests: {len(matches)}")
885+
lines.append("")
886+
887+
# Log detailed info about matched buildrequests and actions taken
888+
if matches:
889+
lines.append("Matches:")
890+
for m in matches:
891+
lines.append(f" - buildrequestid={m['buildrequestid']}")
892+
lines.append(f" buildername={m['buildername']!r}")
893+
lines.append(f" claimed={m['claimed']}")
894+
lines.append(f" complete={m['complete']}")
895+
lines.append(f" submitted_at={m['submitted_at']}")
896+
lines.append(f" branch={m['branch']!r}")
897+
lines.append(f" repository={m['repository']!r}")
898+
lines.append(f" revision={m['revision']!r}")
899+
lines.append(f" codebase={m['codebase']!r}")
900+
if m["url"]:
901+
lines.append(f" url={m['url']}")
902+
lines.append("")
903+
lines.append("Actions:")
904+
lines.extend(f" {a}" for a in actions)
905+
else:
906+
lines.append("No older matching buildrequests found.")
907+
908+
self.addCompleteLog("obsolete-buildrequests", "\n".join(lines) + "\n")
909+
return SUCCESS

0 commit comments

Comments
 (0)