launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #29650
[Merge] ~cjwatson/launchpad:more-relative-build-scores into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:more-relative-build-scores into launchpad:master.
Commit message:
Add relative_build_score to CharmRecipe/GitRepository/Snap
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1974360 in Launchpad itself: "[Feature request] Relative build score for snap recipes"
https://bugs.launchpad.net/launchpad/+bug/1974360
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/437038
This lets us adjust the priority of all builds for a given recipe or repository, which is occasionally useful.
The new attributes are currently only editable by commercial admins and people with similar privilege, which isn't perfect; ideally these would also be editable by Launchpad staff, since they don't allow privilege escalation. However, that would have involved a fair bit more rearrangement (particularly in `GitRepository`, where the `launchpad.Moderate` permission is already in use for something else), and I wanted to get this out the door without blocking on that.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:more-relative-build-scores into launchpad:master.
diff --git a/lib/lp/charms/browser/charmrecipe.py b/lib/lp/charms/browser/charmrecipe.py
index f80aebb..3f5a304 100644
--- a/lib/lp/charms/browser/charmrecipe.py
+++ b/lib/lp/charms/browser/charmrecipe.py
@@ -281,6 +281,7 @@ class ICharmRecipeEditSchema(Interface):
"auto_build",
"auto_build_channels",
"store_upload",
+ "relative_build_score",
],
)
@@ -495,7 +496,10 @@ class CharmRecipeAdminView(BaseCharmRecipeEditView):
page_title = "Administer"
- field_names = ["require_virtualized"]
+ field_names = [
+ "require_virtualized",
+ "relative_build_score",
+ ]
class CharmRecipeEditView(BaseCharmRecipeEditView):
diff --git a/lib/lp/charms/browser/tests/test_charmrecipe.py b/lib/lp/charms/browser/tests/test_charmrecipe.py
index 836fb56..c6568c7 100644
--- a/lib/lp/charms/browser/tests/test_charmrecipe.py
+++ b/lib/lp/charms/browser/tests/test_charmrecipe.py
@@ -565,7 +565,7 @@ class TestCharmRecipeAdminView(BaseTestCharmRecipeView):
)
def test_admin_recipe(self):
- # Admins can change require_virtualized.
+ # Admins can change require_virtualized and relative_build_score.
login("admin@xxxxxxxxxxxxx")
admin = self.factory.makePerson(
member_of=[getUtility(ILaunchpadCelebrities).admin]
@@ -573,14 +573,17 @@ class TestCharmRecipeAdminView(BaseTestCharmRecipeView):
login_person(self.person)
recipe = self.factory.makeCharmRecipe(registrant=self.person)
self.assertTrue(recipe.require_virtualized)
+ self.assertEqual(0, recipe.relative_build_score)
browser = self.getViewBrowser(recipe, user=admin)
browser.getLink("Administer charm recipe").click()
browser.getControl("Require virtualized builders").selected = False
+ browser.getControl("Relative build score").value = "50"
browser.getControl("Update charm recipe").click()
login_admin()
self.assertFalse(recipe.require_virtualized)
+ self.assertEqual(50, recipe.relative_build_score)
def test_admin_recipe_sets_date_last_modified(self):
# Administering a charm recipe sets the date_last_modified property.
diff --git a/lib/lp/charms/interfaces/charmrecipe.py b/lib/lp/charms/interfaces/charmrecipe.py
index 6499998..83f0508 100644
--- a/lib/lp/charms/interfaces/charmrecipe.py
+++ b/lib/lp/charms/interfaces/charmrecipe.py
@@ -764,6 +764,18 @@ class ICharmRecipeAdminAttributes(Interface):
)
)
+ relative_build_score = exported(
+ Int(
+ title=_("Relative build score"),
+ required=True,
+ readonly=False,
+ description=_(
+ "A delta to apply to all build scores for the charm recipe. "
+ "Builds with a higher score will build sooner."
+ ),
+ )
+ )
+
# XXX cjwatson 2021-09-15 bug=760849: "beta" is a lie to get WADL
# generation working. Individual attributes must set their version to
diff --git a/lib/lp/charms/model/charmrecipe.py b/lib/lp/charms/model/charmrecipe.py
index 566b61c..da5e668 100644
--- a/lib/lp/charms/model/charmrecipe.py
+++ b/lib/lp/charms/model/charmrecipe.py
@@ -299,6 +299,8 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
_store_channels = JSON("store_channels", allow_none=True)
+ _relative_build_score = Int(name="relative_build_score", allow_none=True)
+
def __init__(
self,
registrant,
@@ -342,6 +344,7 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
self.store_name = store_name
self.store_secrets = store_secrets
self.store_channels = store_channels
+ self.relative_build_score = 0
def __repr__(self):
return "<CharmRecipe ~%s/%s/+charm/%s>" % (
@@ -393,6 +396,18 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
"""See `ICharmRecipe`."""
self._store_channels = value or None
+ # XXX cjwatson 2023-02-08: Remove this once existing rows have been
+ # backfilled.
+ @property
+ def relative_build_score(self):
+ """See `ICharmRecipe`."""
+ return self._relative_build_score or 0
+
+ @relative_build_score.setter
+ def relative_build_score(self, value):
+ """See `ICharmRecipe`."""
+ self._relative_build_score = value
+
@cachedproperty
def _default_distribution(self):
"""See `ICharmRecipe`."""
diff --git a/lib/lp/charms/model/charmrecipebuild.py b/lib/lp/charms/model/charmrecipebuild.py
index d0656ce..352ccff 100644
--- a/lib/lp/charms/model/charmrecipebuild.py
+++ b/lib/lp/charms/model/charmrecipebuild.py
@@ -237,9 +237,7 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
def calculateScore(self):
"""See `IBuildFarmJob`."""
- # XXX cjwatson 2021-05-28: We'll probably need something like
- # CharmRecipe.relative_build_score at some point.
- return 2510
+ return 2510 + self.recipe.relative_build_score
def getMedianBuildDuration(self):
"""Return the median duration of our successful builds."""
diff --git a/lib/lp/charms/tests/test_charmrecipe.py b/lib/lp/charms/tests/test_charmrecipe.py
index 0855d7e..195de06 100644
--- a/lib/lp/charms/tests/test_charmrecipe.py
+++ b/lib/lp/charms/tests/test_charmrecipe.py
@@ -344,6 +344,17 @@ class TestCharmRecipe(TestCaseWithFactory):
queue_record.score()
self.assertEqual(2510, queue_record.lastscore)
+ def test_requestBuild_relative_build_score(self):
+ # Offsets for charm recipes are respected.
+ recipe = self.factory.makeCharmRecipe()
+ removeSecurityProxy(recipe).relative_build_score = 50
+ das = self.makeBuildableDistroArchSeries()
+ build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
+ build = recipe.requestBuild(build_request, das)
+ queue_record = build.buildqueue_record
+ queue_record.score()
+ self.assertEqual(2560, queue_record.lastscore)
+
def test_requestBuild_channels(self):
# requestBuild can select non-default channels.
recipe = self.factory.makeCharmRecipe()
diff --git a/lib/lp/code/browser/configure.zcml b/lib/lp/code/browser/configure.zcml
index 788c7f2..6978fcb 100644
--- a/lib/lp/code/browser/configure.zcml
+++ b/lib/lp/code/browser/configure.zcml
@@ -865,6 +865,12 @@
template="../templates/git-macros.pt"/>
<browser:page
for="lp.code.interfaces.gitrepository.IGitRepository"
+ class="lp.code.browser.gitrepository.GitRepositoryAdminView"
+ permission="launchpad.Admin"
+ name="+admin"
+ template="../../app/templates/generic-edit.pt"/>
+ <browser:page
+ for="lp.code.interfaces.gitrepository.IGitRepository"
class="lp.code.browser.gitrepository.GitRepositoryEditInformationTypeView"
permission="launchpad.Edit"
name="+edit-information-type"
@@ -877,12 +883,6 @@
template="../../app/templates/generic-edit.pt"/>
<browser:page
for="lp.code.interfaces.gitrepository.IGitRepository"
- class="lp.code.browser.gitrepository.GitRepositoryEditBuilderConstraintsView"
- permission="launchpad.Admin"
- name="+builder-constraints"
- template="../../app/templates/generic-edit.pt"/>
- <browser:page
- for="lp.code.interfaces.gitrepository.IGitRepository"
class="lp.code.browser.gitrepository.GitRepositoryEditView"
permission="launchpad.Edit"
name="+edit"
diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py
index c3724f0..886260c 100644
--- a/lib/lp/code/browser/gitrepository.py
+++ b/lib/lp/code/browser/gitrepository.py
@@ -266,16 +266,21 @@ class GitRepositoryEditMenu(NavigationMenu):
facet = "branches"
title = "Edit Git repository"
links = [
+ "admin",
"edit",
"reviewer",
"permissions",
"activity",
"access_tokens",
"webhooks",
- "builder_constraints",
"delete",
]
+ @enabled_with_permission("launchpad.Admin")
+ def admin(self):
+ text = "Administer repository"
+ return Link("+admin", text, icon="edit")
+
@enabled_with_permission("launchpad.Edit")
def edit(self):
text = "Change repository details"
@@ -306,11 +311,6 @@ class GitRepositoryEditMenu(NavigationMenu):
text = "Manage webhooks"
return Link("+webhooks", text, icon="edit")
- @enabled_with_permission("launchpad.Admin")
- def builder_constraints(self):
- text = "Set builder constraints"
- return Link("+builder-constraints", text, icon="edit")
-
@enabled_with_permission("launchpad.Edit")
def delete(self):
text = "Delete repository"
@@ -634,7 +634,11 @@ class GitRepositoryEditFormView(LaunchpadEditFormView):
use_template(
IGitRepository,
- include=["builder_constraints", "default_branch"],
+ include=[
+ "builder_constraints",
+ "default_branch",
+ "relative_build_score",
+ ],
)
information_type = copy_field(
IGitRepository["information_type"],
@@ -785,10 +789,13 @@ class GitRepositoryEditReviewerView(GitRepositoryEditFormView):
return {"reviewer": self.context.code_reviewer}
-class GitRepositoryEditBuilderConstraintsView(GitRepositoryEditFormView):
- """A view to set builder constraints."""
+class GitRepositoryAdminView(GitRepositoryEditFormView):
+ """A view for administrative settings on repositories."""
- field_names = ["builder_constraints"]
+ field_names = [
+ "builder_constraints",
+ "relative_build_score",
+ ]
custom_widget_builder_constraints = LabeledMultiCheckBoxWidget
diff --git a/lib/lp/code/browser/tests/test_gitrepository.py b/lib/lp/code/browser/tests/test_gitrepository.py
index 51bf64b..ae0c853 100644
--- a/lib/lp/code/browser/tests/test_gitrepository.py
+++ b/lib/lp/code/browser/tests/test_gitrepository.py
@@ -1072,7 +1072,7 @@ class TestGitRepositoryEditReviewerView(TestCaseWithFactory):
self.assertEqual(modified_date, repository.date_last_modified)
-class TestGitRepositoryEditBuilderConstraintsView(BrowserTestCase):
+class TestGitRepositoryAdminView(BrowserTestCase):
layer = DatabaseFunctionalLayer
@@ -1084,24 +1084,23 @@ class TestGitRepositoryEditBuilderConstraintsView(BrowserTestCase):
repository_url = canonical_url(repository, rootsite="code")
browser = self.getViewBrowser(repository, user=owner)
self.assertRaises(
- LinkNotFoundError, browser.getLink, "Set builder constraints"
+ LinkNotFoundError, browser.getLink, "Administer repository"
)
self.assertRaises(
Unauthorized,
self.getUserBrowser,
- repository_url + "/+builder-constraints",
+ repository_url + "/+admin",
user=owner,
)
def test_commercial_admin(self):
- # A commercial admin can set builder constraints on a Git
- # repository.
+ # A commercial admin can administer a Git repository.
self.factory.makeBuilder(open_resources=["gpu", "large", "another"])
owner = self.factory.makePerson()
repository = self.factory.makeGitRepository(owner=owner)
commercial_admin = login_celebrity("commercial_admin")
browser = self.getViewBrowser(repository, user=commercial_admin)
- browser.getLink("Set builder constraints").click()
+ browser.getLink("Administer repository").click()
builder_constraints = browser.getControl(
name="field.builder_constraints"
)
@@ -1116,10 +1115,14 @@ class TestGitRepositoryEditBuilderConstraintsView(BrowserTestCase):
"gpu",
"large",
]
+ relative_build_score = browser.getControl("Relative build score")
+ self.assertEqual("0", relative_build_score.value)
+ relative_build_score.value = "50"
browser.getControl("Change Git Repository").click()
login_person(owner)
self.assertEqual(("gpu", "large"), repository.builder_constraints)
+ self.assertEqual(50, repository.relative_build_score)
class TestGitRepositoryEditView(TestCaseWithFactory):
diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py
index 453954f..933e824 100644
--- a/lib/lp/code/interfaces/gitrepository.py
+++ b/lib/lp/code/interfaces/gitrepository.py
@@ -1286,6 +1286,18 @@ class IGitRepositoryAdminAttributes(Interface):
as_of="devel",
)
+ relative_build_score = exported(
+ Int(
+ title=_("Relative build score"),
+ required=True,
+ readonly=False,
+ description=_(
+ "A delta to apply to all build scores of this repository. "
+ "Builds with a higher score will build sooner."
+ ),
+ )
+ )
+
# XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL
# generation working. Individual attributes must set their version to
diff --git a/lib/lp/code/model/cibuild.py b/lib/lp/code/model/cibuild.py
index 0bfc773..f70592d 100644
--- a/lib/lp/code/model/cibuild.py
+++ b/lib/lp/code/model/cibuild.py
@@ -322,7 +322,7 @@ class CIBuild(PackageBuildMixin, StormBase):
# above bulky things like live filesystem builds, but below
# important things like builds of proposed Ubuntu stable updates.
# See https://help.launchpad.net/Packaging/BuildScores.
- return 2600
+ return 2600 + self.git_repository.relative_build_score
def getMedianBuildDuration(self):
"""Return the median duration of our recent successful builds."""
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index f78c20c..8369224 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -328,6 +328,8 @@ class GitRepository(
name="builder_constraints", allow_none=True
)
+ _relative_build_score = Int(name="relative_build_score", allow_none=True)
+
def __init__(
self,
repository_type,
@@ -378,6 +380,7 @@ class GitRepository(
self.date_last_repacked = date_last_repacked
self.date_last_scanned = date_last_scanned
self.builder_constraints = builder_constraints
+ self.relative_build_score = 0
def _createOnHostingService(
self, clone_from_repository=None, async_create=False
@@ -465,6 +468,18 @@ class GitRepository(
else:
return self.owner
+ # XXX cjwatson 2023-02-08: Remove this once existing rows have been
+ # backfilled.
+ @property
+ def relative_build_score(self):
+ """See `IGitRepository`."""
+ return self._relative_build_score or 0
+
+ @relative_build_score.setter
+ def relative_build_score(self, value):
+ """See `IGitRepository`."""
+ self._relative_build_score = value
+
def _checkPersonalPrivateOwnership(self, new_owner):
if self.information_type in PRIVATE_INFORMATION_TYPES and (
not new_owner.is_team
diff --git a/lib/lp/code/model/tests/test_cibuild.py b/lib/lp/code/model/tests/test_cibuild.py
index d5b4405..dd2a0d1 100644
--- a/lib/lp/code/model/tests/test_cibuild.py
+++ b/lib/lp/code/model/tests/test_cibuild.py
@@ -702,6 +702,19 @@ class TestCIBuildSet(TestCaseWithFactory):
queue_record.score()
self.assertEqual(2600, queue_record.lastscore)
+ def test_requestBuild_relative_build_score(self):
+ # Offsets for Git repositories are respected.
+ repository = self.factory.makeGitRepository()
+ removeSecurityProxy(repository).relative_build_score = 50
+ commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest()
+ das = self.factory.makeBuildableDistroArchSeries()
+ build = getUtility(ICIBuildSet).requestBuild(
+ repository, commit_sha1, das, [[("test", 0)]]
+ )
+ queue_record = build.buildqueue_record
+ queue_record.score()
+ self.assertEqual(2650, queue_record.lastscore)
+
def test_requestBuild_rejects_repeats(self):
# requestBuild refuses if an identical build was already requested.
repository = self.factory.makeGitRepository()
diff --git a/lib/lp/scripts/garbo.py b/lib/lp/scripts/garbo.py
index e798587..bcd8da6 100644
--- a/lib/lp/scripts/garbo.py
+++ b/lib/lp/scripts/garbo.py
@@ -57,6 +57,7 @@ from lp.bugs.scripts.checkwatches.scheduler import (
MAX_SAMPLE_SIZE,
BugWatchScheduler,
)
+from lp.charms.model.charmrecipe import CharmRecipe
from lp.code.enums import GitRepositoryStatus, RevisionStatusArtifactType
from lp.code.interfaces.revision import IRevisionSet
from lp.code.model.codeimportevent import CodeImportEvent
@@ -117,6 +118,7 @@ from lp.services.verification.model.logintoken import LoginToken
from lp.services.webapp.publisher import canonical_url
from lp.services.webhooks.interfaces import IWebhookJobSource
from lp.services.webhooks.model import WebhookJob
+from lp.snappy.model.snap import Snap
from lp.snappy.model.snapbuild import SnapFile
from lp.snappy.model.snapbuildjob import SnapBuildJobType
from lp.soyuz.enums import (
@@ -2243,6 +2245,88 @@ class ArchiveFileDatePopulator(TunableLoop):
transaction.commit()
+class CharmRecipeRelativeBuildScorePopulator(TunableLoop):
+ """Populates CharmRecipe.relative_build_score."""
+
+ maximum_chunk_size = 5000
+
+ def __init__(self, log, abort_time=None):
+ super().__init__(log, abort_time)
+ self.start_at = 1
+ self.store = IPrimaryStore(CharmRecipe)
+
+ def findRecipes(self):
+ return self.store.find(
+ CharmRecipe,
+ CharmRecipe.id >= self.start_at,
+ CharmRecipe._relative_build_score == None,
+ ).order_by(CharmRecipe.id)
+
+ def isDone(self):
+ return self.findRecipes().is_empty()
+
+ def __call__(self, chunk_size):
+ recipes = list(self.findRecipes()[:chunk_size])
+ for recipe in recipes:
+ recipe.relative_build_score = 0
+ self.start_at = recipes[-1].id + 1
+ transaction.commit()
+
+
+class GitRepositoryRelativeBuildScorePopulator(TunableLoop):
+ """Populates GitRepository.relative_build_score."""
+
+ maximum_chunk_size = 5000
+
+ def __init__(self, log, abort_time=None):
+ super().__init__(log, abort_time)
+ self.start_at = 1
+ self.store = IPrimaryStore(GitRepository)
+
+ def findRepositories(self):
+ return self.store.find(
+ GitRepository,
+ GitRepository.id >= self.start_at,
+ GitRepository._relative_build_score == None,
+ ).order_by(GitRepository.id)
+
+ def isDone(self):
+ return self.findRepositories().is_empty()
+
+ def __call__(self, chunk_size):
+ repositories = list(self.findRepositories()[:chunk_size])
+ for repository in repositories:
+ repository.relative_build_score = 0
+ self.start_at = repositories[-1].id + 1
+ transaction.commit()
+
+
+class SnapRelativeBuildScorePopulator(TunableLoop):
+ """Populates Snap.relative_build_score."""
+
+ maximum_chunk_size = 5000
+
+ def __init__(self, log, abort_time=None):
+ super().__init__(log, abort_time)
+ self.start_at = 1
+ self.store = IPrimaryStore(Snap)
+
+ def findRecipes(self):
+ return self.store.find(
+ Snap, Snap.id >= self.start_at, Snap._relative_build_score == None
+ ).order_by(Snap.id)
+
+ def isDone(self):
+ return self.findRecipes().is_empty()
+
+ def __call__(self, chunk_size):
+ recipes = list(self.findRecipes()[:chunk_size])
+ for recipe in recipes:
+ recipe.relative_build_score = 0
+ self.start_at = recipes[-1].id + 1
+ transaction.commit()
+
+
class BaseDatabaseGarbageCollector(LaunchpadCronScript):
"""Abstract base class to run a collection of TunableLoops."""
@@ -2562,10 +2646,12 @@ class DailyDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
BranchJobPruner,
BugNotificationPruner,
BugWatchActivityPruner,
+ CharmRecipeRelativeBuildScorePopulator,
CodeImportEventPruner,
CodeImportResultPruner,
DiffPruner,
GitJobPruner,
+ GitRepositoryRelativeBuildScorePopulator,
LiveFSFilePruner,
LoginTokenPruner,
OCIFilePruner,
@@ -2579,6 +2665,7 @@ class DailyDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
ScrubPOFileTranslator,
SnapBuildJobPruner,
SnapFilePruner,
+ SnapRelativeBuildScorePopulator,
SourcePackagePublishingHistoryFormatPopulator,
SuggestiveTemplatesCacheUpdater,
TeamMembershipPruner,
diff --git a/lib/lp/scripts/tests/test_garbo.py b/lib/lp/scripts/tests/test_garbo.py
index eae9163..6d531c2 100644
--- a/lib/lp/scripts/tests/test_garbo.py
+++ b/lib/lp/scripts/tests/test_garbo.py
@@ -42,6 +42,7 @@ from lp.bugs.model.bugnotification import (
BugNotificationRecipient,
)
from lp.buildmaster.enums import BuildStatus
+from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
from lp.code.bzr import BranchFormat, RepositoryFormat
from lp.code.enums import (
CodeImportResultStatus,
@@ -2545,6 +2546,101 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
self.assertEqual(1, rs.count())
self.assertEqual(archive_files[1], rs.one())
+ def test_CharmRecipeRelativeBuildScorePopulator(self):
+ self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
+ switch_dbuser("testadmin")
+ old_recipes = [self.factory.makeCharmRecipe() for _ in range(2)]
+ for recipe in old_recipes:
+ removeSecurityProxy(recipe)._relative_build_score = None
+ try:
+ Store.of(old_recipes[0]).flush()
+ except IntegrityError:
+ # Now enforced by DB NOT NULL constraint; backfilling is no
+ # longer necessary.
+ return
+ new_recipes = [self.factory.makeCharmRecipe() for _ in range(2)]
+ self.assertEqual(0, new_recipes[0].relative_build_score)
+ new_recipes[1].relative_build_score = 100
+ transaction.commit()
+
+ self.runDaily()
+
+ # Old recipes are backfilled.
+ for recipe in old_recipes:
+ self.assertEqual(
+ 0, removeSecurityProxy(recipe)._relative_build_score
+ )
+ # Other recipes are left alone.
+ self.assertEqual(
+ 0, removeSecurityProxy(new_recipes[0])._relative_build_score
+ )
+ self.assertEqual(
+ 100, removeSecurityProxy(new_recipes[1])._relative_build_score
+ )
+
+ def test_GitRepositoryRelativeBuildScorePopulator(self):
+ switch_dbuser("testadmin")
+ old_repositories = [self.factory.makeGitRepository() for _ in range(2)]
+ for repository in old_repositories:
+ removeSecurityProxy(repository)._relative_build_score = None
+ try:
+ Store.of(old_repositories[0]).flush()
+ except IntegrityError:
+ # Now enforced by DB NOT NULL constraint; backfilling is no
+ # longer necessary.
+ return
+ new_repositories = [self.factory.makeGitRepository() for _ in range(2)]
+ self.assertEqual(0, new_repositories[0].relative_build_score)
+ new_repositories[1].relative_build_score = 100
+ transaction.commit()
+
+ self.runDaily()
+
+ # Old repositories are backfilled.
+ for repository in old_repositories:
+ self.assertEqual(
+ 0, removeSecurityProxy(repository)._relative_build_score
+ )
+ # Other repositories are left alone.
+ self.assertEqual(
+ 0, removeSecurityProxy(new_repositories[0])._relative_build_score
+ )
+ self.assertEqual(
+ 100,
+ removeSecurityProxy(new_repositories[1])._relative_build_score,
+ )
+
+ def test_SnapRelativeBuildScorePopulator(self):
+ switch_dbuser("testadmin")
+ old_recipes = [self.factory.makeSnap() for _ in range(2)]
+ for recipe in old_recipes:
+ removeSecurityProxy(recipe)._relative_build_score = None
+ try:
+ Store.of(old_recipes[0]).flush()
+ except IntegrityError:
+ # Now enforced by DB NOT NULL constraint; backfilling is no
+ # longer necessary.
+ return
+ new_recipes = [self.factory.makeSnap() for _ in range(2)]
+ self.assertEqual(0, new_recipes[0].relative_build_score)
+ new_recipes[1].relative_build_score = 100
+ transaction.commit()
+
+ self.runDaily()
+
+ # Old recipes are backfilled.
+ for recipe in old_recipes:
+ self.assertEqual(
+ 0, removeSecurityProxy(recipe)._relative_build_score
+ )
+ # Other recipes are left alone.
+ self.assertEqual(
+ 0, removeSecurityProxy(new_recipes[0])._relative_build_score
+ )
+ self.assertEqual(
+ 100, removeSecurityProxy(new_recipes[1])._relative_build_score
+ )
+
class TestGarboTasks(TestCaseWithFactory):
layer = LaunchpadZopelessLayer
diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py
index 80819c0..71602fe 100644
--- a/lib/lp/snappy/browser/snap.py
+++ b/lib/lp/snappy/browser/snap.py
@@ -496,6 +496,7 @@ class ISnapEditSchema(Interface):
"auto_build",
"auto_build_channels",
"store_upload",
+ "relative_build_score",
],
)
@@ -915,6 +916,7 @@ class SnapAdminView(BaseSnapEditView):
"information_type",
"require_virtualized",
"allow_internet",
+ "relative_build_score",
]
@property
diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
index 4c2fbdd..2cbfa05 100644
--- a/lib/lp/snappy/browser/tests/test_snap.py
+++ b/lib/lp/snappy/browser/tests/test_snap.py
@@ -849,7 +849,8 @@ class TestSnapAdminView(BaseTestSnapView):
)
def test_admin_snap(self):
- # Admins can change require_virtualized, privacy, and allow_internet.
+ # Admins can change require_virtualized, privacy, allow_internet,
+ # and relative_build_score.
login("admin@xxxxxxxxxxxxx")
admin = self.factory.makePerson(
member_of=[getUtility(ILaunchpadCelebrities).admin]
@@ -863,6 +864,7 @@ class TestSnapAdminView(BaseTestSnapView):
self.assertIsNone(snap.project)
self.assertFalse(snap.private)
self.assertTrue(snap.allow_internet)
+ self.assertEqual(0, snap.relative_build_score)
self.factory.makeAccessPolicy(
pillar=project, type=InformationType.PRIVATESECURITY
@@ -874,6 +876,7 @@ class TestSnapAdminView(BaseTestSnapView):
browser.getControl("Require virtualized builders").selected = False
browser.getControl(name="field.information_type").value = private
browser.getControl("Allow external network access").selected = False
+ browser.getControl("Relative build score").value = "50"
browser.getControl("Update snap package").click()
login_admin()
@@ -881,6 +884,7 @@ class TestSnapAdminView(BaseTestSnapView):
self.assertFalse(snap.require_virtualized)
self.assertTrue(snap.private)
self.assertFalse(snap.allow_internet)
+ self.assertEqual(50, snap.relative_build_score)
def test_admin_snap_private_without_project(self):
# Cannot make snap private if it doesn't have a project associated.
diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
index a86919c..55ba182 100644
--- a/lib/lp/snappy/interfaces/snap.py
+++ b/lib/lp/snappy/interfaces/snap.py
@@ -1198,6 +1198,18 @@ class ISnapAdminAttributes(Interface):
)
)
+ relative_build_score = exported(
+ Int(
+ title=_("Relative build score"),
+ required=True,
+ readonly=False,
+ description=_(
+ "A delta to apply to all build scores for the snap recipe. "
+ "Builds with a higher score will build sooner."
+ ),
+ )
+ )
+
def subscribe(person, subscribed_by):
"""Subscribe a person to this snap recipe."""
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index de5e86e..0b7d0f8 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -394,6 +394,8 @@ class Snap(StormBase, WebhookTargetMixin):
_store_channels = JSON("store_channels", allow_none=True)
+ _relative_build_score = Int(name="relative_build_score", allow_none=True)
+
def __init__(
self,
registrant,
@@ -452,6 +454,7 @@ class Snap(StormBase, WebhookTargetMixin):
self.store_name = store_name
self.store_secrets = store_secrets
self.store_channels = store_channels
+ self.relative_build_score = 0
def __repr__(self):
return "<Snap ~%s/+snap/%s>" % (self.owner.name, self.name)
@@ -679,6 +682,18 @@ class Snap(StormBase, WebhookTargetMixin):
def store_channels(self, value):
self._store_channels = value or None
+ # XXX cjwatson 2023-02-08: Remove this once existing rows have been
+ # backfilled.
+ @property
+ def relative_build_score(self):
+ """See `ISnap`."""
+ return self._relative_build_score or 0
+
+ @relative_build_score.setter
+ def relative_build_score(self, value):
+ """See `ISnap`."""
+ self._relative_build_score = value
+
def getAllowedInformationTypes(self, user):
"""See `ISnap`."""
if user_has_special_snap_access(user):
diff --git a/lib/lp/snappy/model/snapbuild.py b/lib/lp/snappy/model/snapbuild.py
index 410942b..e6678c0 100644
--- a/lib/lp/snappy/model/snapbuild.py
+++ b/lib/lp/snappy/model/snapbuild.py
@@ -304,7 +304,11 @@ class SnapBuild(PackageBuildMixin, StormBase):
return super().can_be_retried
def calculateScore(self):
- return 2510 + self.archive.relative_build_score
+ return (
+ 2510
+ + self.archive.relative_build_score
+ + self.snap.relative_build_score
+ )
def getMedianBuildDuration(self):
"""Return the median duration of our successful builds."""
diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
index 93fe69d..f2ef159 100644
--- a/lib/lp/snappy/tests/test_snap.py
+++ b/lib/lp/snappy/tests/test_snap.py
@@ -278,7 +278,7 @@ class TestSnap(TestCaseWithFactory):
self.assertEqual(2510, queue_record.lastscore)
def test_requestBuild_relative_build_score(self):
- # Offsets for archives are respected.
+ # Offsets for archives and snap recipes are respected.
processor = self.factory.makeProcessor(supports_virtualized=True)
distroarchseries = self.makeBuildableDistroArchSeries(
processor=processor
@@ -286,6 +286,7 @@ class TestSnap(TestCaseWithFactory):
snap = self.factory.makeSnap(
distroseries=distroarchseries.distroseries, processors=[processor]
)
+ removeSecurityProxy(snap).relative_build_score = 50
archive = self.factory.makeArchive(owner=snap.owner)
removeSecurityProxy(archive).relative_build_score = 100
build = snap.requestBuild(
@@ -296,7 +297,7 @@ class TestSnap(TestCaseWithFactory):
)
queue_record = build.buildqueue_record
queue_record.score()
- self.assertEqual(2610, queue_record.lastscore)
+ self.assertEqual(2660, queue_record.lastscore)
def test_requestBuild_snap_base(self):
# requestBuild can select a snap base.