@@ -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