Skip to content

Commit a5646ff

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 f46e458 commit a5646ff

2 files changed

Lines changed: 268 additions & 0 deletions

File tree

master-protected-branches/master.cfg

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ from locks import getLocks
1111
from master_common import base_master_config
1212
from utils import (
1313
CancelDuplicateBuildRequests,
14+
CancelOlderSameBranchRequests,
1415
canStartBuild,
1516
createWorker,
1617
isJepsenBranch,
@@ -202,6 +203,16 @@ f_tarball = util.BuildFactory()
202203
f_tarball.addStep(
203204
CancelDuplicateBuildRequests(buildbot_base_url=os.environ.get("BUILDMASTER_URL"))
204205
)
206+
207+
f_tarball.addStep(
208+
CancelOlderSameBranchRequests(
209+
dry_run=False,
210+
same_builder_only=False,
211+
cancel_claimed=True,
212+
buildbot_base_url=os.environ.get("BUILDMASTER_URL"),
213+
)
214+
)
215+
205216
f_tarball.addStep(
206217
steps.ShellCommand(command=["echo", " revision: ", util.Property("revision")])
207218
)

utils.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,3 +897,260 @@ def run(self):
897897

898898
self.addCompleteLog("duplicate-buildrequests", "\n".join(lines) + "\n")
899899
return SUCCESS
900+
901+
902+
# TODO: Upgrading buildbot to 4.* deprecates this class
903+
# Use instead the OldBuildCanceller service
904+
# https://docs.buildbot.net/latest/manual/configuration/services/old_build_canceller.html
905+
class CancelOlderSameBranchRequests(BuildStep):
906+
name = "cancel older obsolete buildrequests"
907+
description = ["checking older matching requests"]
908+
descriptionDone = ["older matching requests checked"]
909+
910+
def __init__(
911+
self,
912+
dry_run=False,
913+
same_builder_only=False,
914+
cancel_claimed=True,
915+
buildbot_base_url=None,
916+
**kwargs,
917+
):
918+
super().__init__(**kwargs)
919+
self.dry_run = dry_run
920+
self.same_builder_only = same_builder_only
921+
self.cancel_claimed = cancel_claimed
922+
self.buildbot_base_url = (
923+
buildbot_base_url.rstrip("/") if buildbot_base_url else None
924+
)
925+
self._builder_name_cache = {}
926+
927+
def _buildrequest_url(self, brid):
928+
if not self.buildbot_base_url:
929+
return None
930+
return f"{self.buildbot_base_url}/#/buildrequests/{brid}"
931+
932+
@defer.inlineCallbacks
933+
def _builder_name(self, builderid):
934+
if builderid in self._builder_name_cache:
935+
return self._builder_name_cache[builderid]
936+
937+
builder = yield self.master.data.get(("builders", builderid))
938+
name = builder.get("name", f"<builderid={builderid}>")
939+
self._builder_name_cache[builderid] = name
940+
return name
941+
942+
@staticmethod
943+
def _fmt_ss(ss):
944+
return (
945+
f"branch={ss.get('branch')!r}, "
946+
f"repository={ss.get('repository')!r}, "
947+
f"revision={ss.get('revision')!r}, "
948+
f"codebase={ss.get('codebase', '')!r}"
949+
)
950+
951+
@defer.inlineCallbacks
952+
def run(self):
953+
current_buildid = self.build.buildid
954+
955+
current_build = yield self.master.data.get(("builds", current_buildid))
956+
current_buildrequestid = current_build["buildrequestid"]
957+
current_builderid = current_build["builderid"]
958+
current_buildername = yield self._builder_name(current_builderid)
959+
960+
current_buildrequest = yield self.master.data.get(
961+
("buildrequests", current_buildrequestid)
962+
)
963+
current_buildsetid = current_buildrequest["buildsetid"]
964+
965+
current_buildset = yield self.master.data.get(("buildsets", current_buildsetid))
966+
current_submitted_at = current_buildset.get("submitted_at")
967+
current_sourcestamps = current_buildset.get("sourcestamps", [])
968+
969+
if current_submitted_at is None or not current_sourcestamps:
970+
self.addCompleteLog(
971+
"summary",
972+
"Current buildset is missing submitted_at or sourcestamps; nothing to do.\n",
973+
)
974+
return SUCCESS
975+
976+
# We want only running or in queue buildrequests
977+
filters = [Filter("complete", "eq", [False])]
978+
# Narrow the search to cancel only buildrequests for the calling builder
979+
if self.same_builder_only:
980+
filters.append(Filter("builderid", "eq", [current_builderid]))
981+
982+
# Getting all buildrequests based on filters
983+
buildrequests = yield self.master.data.get(
984+
("buildrequests",),
985+
filters=filters,
986+
fields=[
987+
"buildrequestid",
988+
"buildsetid",
989+
"builderid",
990+
"claimed",
991+
"complete",
992+
"submitted_at",
993+
],
994+
)
995+
996+
# Log info about the current build
997+
lines = []
998+
lines.append(f"Mode: {'DRY-RUN' if self.dry_run else 'ACTIVE'}")
999+
lines.append(f"same_builder_only={self.same_builder_only}")
1000+
lines.append(f"cancel_claimed={self.cancel_claimed}")
1001+
lines.append("")
1002+
lines.append("Current:")
1003+
lines.append(f" buildid={current_buildid}")
1004+
lines.append(f" buildrequestid={current_buildrequestid}")
1005+
lines.append(f" builderid={current_builderid}")
1006+
lines.append(f" buildername={current_buildername!r}")
1007+
lines.append(f" buildsetid={current_buildsetid}")
1008+
lines.append(f" submitted_at={current_submitted_at}")
1009+
current_url = self._buildrequest_url(current_buildrequestid)
1010+
if current_url:
1011+
lines.append(f" url={current_url}")
1012+
lines.append(" sourcestamps:")
1013+
for i, ss in enumerate(current_sourcestamps, 1):
1014+
lines.append(f" [{i}] {self._fmt_ss(ss)}")
1015+
lines.append("")
1016+
1017+
matches = []
1018+
actions = []
1019+
cancel_errors = []
1020+
1021+
for br in buildrequests:
1022+
brid = br["buildrequestid"]
1023+
1024+
# Skip self
1025+
if brid == current_buildrequestid:
1026+
continue
1027+
1028+
# Skip cancelling running builds if cancel_claimed is False
1029+
if not self.cancel_claimed and br.get("claimed"):
1030+
continue
1031+
1032+
other_buildsetid = br["buildsetid"]
1033+
other_buildset = yield self.master.data.get(("buildsets", other_buildsetid))
1034+
other_submitted_at = other_buildset.get("submitted_at")
1035+
other_sourcestamps = other_buildset.get("sourcestamps", [])
1036+
1037+
if other_submitted_at is None:
1038+
continue
1039+
1040+
# Newest wins: only cancel OLDER matching requests
1041+
if other_submitted_at >= current_submitted_at:
1042+
continue
1043+
1044+
# A match means same branch+repository+codebase but different revision
1045+
matched_other_ss = None
1046+
1047+
# If the buildset can have multiple sourcestamps
1048+
for current_ss in current_sourcestamps:
1049+
for other_ss in other_sourcestamps:
1050+
same_target = (
1051+
other_ss.get("codebase", "") == current_ss.get("codebase", "")
1052+
and other_ss.get("repository") == current_ss.get("repository")
1053+
and other_ss.get("branch") == current_ss.get("branch")
1054+
)
1055+
different_revision = other_ss.get("revision") != current_ss.get(
1056+
"revision"
1057+
)
1058+
1059+
if same_target and different_revision:
1060+
matched_other_ss = other_ss
1061+
break
1062+
if matched_other_ss is not None:
1063+
break
1064+
1065+
if matched_other_ss is None:
1066+
continue
1067+
1068+
other_builderid = br["builderid"]
1069+
other_buildername = yield self._builder_name(other_builderid)
1070+
1071+
info = {
1072+
"buildrequestid": brid,
1073+
"buildername": other_buildername,
1074+
"claimed": br.get("claimed"),
1075+
"complete": br.get("complete"),
1076+
"submitted_at": other_submitted_at,
1077+
"branch": matched_other_ss.get("branch"),
1078+
"repository": matched_other_ss.get("repository"),
1079+
"revision": matched_other_ss.get("revision"),
1080+
"codebase": matched_other_ss.get("codebase", ""),
1081+
"url": self._buildrequest_url(brid),
1082+
}
1083+
matches.append(info)
1084+
1085+
# Dry-run mode doesn't actually cancel, just log what would be cancelled
1086+
if self.dry_run:
1087+
msg = (
1088+
f"[DRY-RUN] would cancel buildrequest {brid} "
1089+
f"(buildername={other_buildername!r}, "
1090+
f"claimed={br.get('claimed')}, "
1091+
f"submitted_at={other_submitted_at}, "
1092+
f"revision={matched_other_ss.get('revision')!r})"
1093+
)
1094+
if info["url"]:
1095+
msg += f" url={info['url']}"
1096+
actions.append(msg)
1097+
else:
1098+
try:
1099+
yield self.master.data.control(
1100+
"cancel",
1101+
{"reason": ("Superseded by newer build for same branch")},
1102+
("buildrequests", brid),
1103+
)
1104+
except Exception as e:
1105+
msg = (
1106+
f"Failed to request cancel for buildrequest {brid} "
1107+
f"(buildername={other_buildername!r}, "
1108+
f"claimed={br.get('claimed')}, "
1109+
f"submitted_at={other_submitted_at}, "
1110+
f"revision={matched_other_ss.get('revision')!r}, "
1111+
f"error={e!r})"
1112+
)
1113+
if info["url"]:
1114+
msg += f" url={info['url']}"
1115+
actions.append(msg)
1116+
cancel_errors.append(msg)
1117+
log.err(e, f"Failed to request cancel for buildrequest {brid}")
1118+
else:
1119+
msg = (
1120+
f"Requested cancel for buildrequest {brid} "
1121+
f"(buildername={other_buildername!r}, "
1122+
f"claimed={br.get('claimed')}, "
1123+
f"submitted_at={other_submitted_at}, "
1124+
f"revision={matched_other_ss.get('revision')!r})"
1125+
)
1126+
if info["url"]:
1127+
msg += f" url={info['url']}"
1128+
actions.append(msg)
1129+
1130+
lines.append(f"Matched older buildrequests: {len(matches)}")
1131+
lines.append(f"Cancel request failures: {len(cancel_errors)}")
1132+
lines.append("")
1133+
1134+
# Log detailed info about matched buildrequests and actions taken
1135+
if matches:
1136+
lines.append("Matches:")
1137+
for m in matches:
1138+
lines.append(f" - buildrequestid={m['buildrequestid']}")
1139+
lines.append(f" buildername={m['buildername']!r}")
1140+
lines.append(f" claimed={m['claimed']}")
1141+
lines.append(f" complete={m['complete']}")
1142+
lines.append(f" submitted_at={m['submitted_at']}")
1143+
lines.append(f" branch={m['branch']!r}")
1144+
lines.append(f" repository={m['repository']!r}")
1145+
lines.append(f" revision={m['revision']!r}")
1146+
lines.append(f" codebase={m['codebase']!r}")
1147+
if m["url"]:
1148+
lines.append(f" url={m['url']}")
1149+
lines.append("")
1150+
lines.append("Actions:")
1151+
lines.extend(f" {a}" for a in actions)
1152+
else:
1153+
lines.append("No older matching buildrequests found.")
1154+
1155+
self.addCompleteLog("obsolete-buildrequests", "\n".join(lines) + "\n")
1156+
return SUCCESS

0 commit comments

Comments
 (0)