Merge ~ilasc/launchpad:add-get-build-by-store-revision-to-snap into launchpad:master

Proposed by Ioana Lasc
Status: Rejected
Rejected by: Ioana Lasc
Proposed branch: ~ilasc/launchpad:add-get-build-by-store-revision-to-snap
Merge into: launchpad:master
Diff against target: 117 lines (+68/-2)
3 files modified
lib/lp/snappy/interfaces/snap.py (+15/-0)
lib/lp/snappy/model/snap.py (+9/-0)
lib/lp/snappy/tests/test_snap.py (+44/-2)
Reviewer Review Type Date Requested Status
Colin Watson (community) Needs Fixing
Review via email: mp+407250@code.launchpad.net

Commit message

Add getBuildByStoreRevision to Snap

To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) wrote :

For the query I tried using the _getBuilds method with a "filter_term = (SnapBuild.store_upload_revision == store_upload_revision)" but that will return an empty set, I assume because store_upload_revision is property and not a persisted attribute ?

Revision history for this message
Colin Watson (cjwatson) wrote :
Download full text (3.2 KiB)

My apologies for blithely saying this was just a simple query; I'd forgotten about the way in which store revisions are currently stored. This may be a bit more work than I initially thought. On the other hand: useful database engineering exercise, I guess! As penance, I've walked through my thought process in detail here.

You'll definitely need to find an alternative approach, as walking through all the builds for the snap recipe in Python will be much too slow for snap recipes with more than a few builds.

First, consider doing this in a single PostgreSQL query. This will be a bit fiddly because conceptually what you want is to join with `SnapBuildJob` but only consider those cases from the join where the `SnapBuildJob` is the one with the highest job ID for each snap recipe, and then look at the union of `SnapBuildJob.metadata` and `SnapBuild.store_upload_metadata` and extract the `store_revision` field from that: this emulates the behaviour of the `SnapBuild.store_upload_revision` property (drilling down into `SnapBuild.last_store_upload_job`, `SnapStoreUploadJob.store_revision`, and `SnapStoreUploadJob.store_metadata`). The tools you'd need for this would be PostgreSQL window functions (https://www.postgresql.org/docs/10/tutorial-window.html) and JSON functions (https://www.postgresql.org/docs/10/functions-json.html); a relevant example of using window functions is in `lp.scripts.garbo.SnapBuildJobPruner`.

Looking at this, though, I'm a bit concerned that it may not be possible to implement it efficiently with the current database schema, because PostgreSQL is going to have to walk through all the builds and jobs internally and extract all their JSON metadata fields, then cast them to `jsonb` (since these columns predate us being able to use PostgreSQL's `jsonb` type and are thus just `text`), concatenate `SnapBuildJob.metadata` and `SnapBuild.store_upload_metadata`, and extract the appropriate field from the result. I'm not convinced that it will be possible to construct an index that allows PostgreSQL to do that efficiently for snap recipes with large numbers of builds. It's probably worth having a go at constructing the appropriate query and throwing it at `EXPLAIN ANALYZE` on a database with a respectable number of builds for the relevant snap recipe to see if it can be indexed usefully, if nothing else because window functions are sometimes handy and it's a good opportunity to learn about them. However, there seems a good chance that this won't work out, so I'd advise timeboxing that exercise: no more than half a day or so.

So, the fallback option: let's consider denormalizing `SnapBuild.store_upload_revision` (that is, adding an explicit database column for it on `SnapBuild` that's maintained by either PostgreSQL trigger functions or more likely by Python code called from anywhere that might alter the result of the current `SnapBuild.store_upload_revision` property). We could then add an index on `SnapBuild (snap, store_upload_revision)` that would efficiently serve this query. Note that we'll need to backfill this column using a garbo job before it can be used. This approach seems much more likely to work, since it k...

Read more...

review: Needs Fixing
Revision history for this message
Ioana Lasc (ilasc) wrote :

The work for this has been split into several tickets and MPs and will mark this as rejected. The new MP for this is: https://code.launchpad.net/~ilasc/launchpad/+git/launchpad/+merge/407781

Unmerged commits

079a40b... by Ioana Lasc

Add getBuildByStoreRevision to Snap

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
index 2445553..400b92e 100644
--- a/lib/lp/snappy/interfaces/snap.py
+++ b/lib/lp/snappy/interfaces/snap.py
@@ -529,6 +529,21 @@ class ISnapView(Interface):
529529
530 @call_with(user=REQUEST_USER)530 @call_with(user=REQUEST_USER)
531 @operation_parameters(531 @operation_parameters(
532 store_upload_revision=Int(title="Store revision",
533 required=True))
534 @export_read_operation()
535 @operation_for_version("devel")
536 def getBuildByStoreRevision(store_upload_revision, user=None):
537 """Returns the build (if any) of that snap recipe
538 that has the given store_upload_revision.
539
540 :param store_upload_revision: A list of snap build request IDs.
541 :param user: The `IPerson` requesting this information.
542 :return: An 'ISnapBuild' or None.
543 """
544
545 @call_with(user=REQUEST_USER)
546 @operation_parameters(
532 request_ids=List(547 request_ids=List(
533 title=_("A list of snap build request IDs."), value_type=Int(),548 title=_("A list of snap build request IDs."), value_type=Int(),
534 required=False),549 required=False),
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index 24aa2be..fa20509 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -1111,6 +1111,15 @@ class Snap(Storm, WebhookTargetMixin):
11111111
1112 return result1112 return result
11131113
1114 def getBuildByStoreRevision(self, store_upload_revision):
1115 results = list(Store.of(self).find(
1116 SnapBuild,
1117 SnapBuild.snap == self))
1118 for build in results:
1119 if build.store_upload_revision == store_upload_revision:
1120 return build
1121 return None
1122
1114 @property1123 @property
1115 def builds(self):1124 def builds(self):
1116 """See `ISnap`."""1125 """See `ISnap`."""
diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
index 4d9c92f..b64ddc4 100644
--- a/lib/lp/snappy/tests/test_snap.py
+++ b/lib/lp/snappy/tests/test_snap.py
@@ -124,8 +124,8 @@ from lp.snappy.interfaces.snapbase import (
124 )124 )
125from lp.snappy.interfaces.snapbuild import (125from lp.snappy.interfaces.snapbuild import (
126 ISnapBuild,126 ISnapBuild,
127 ISnapBuildSet,127 ISnapBuildSet, SnapBuildStoreUploadStatus,
128 )128)
129from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource129from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource
130from lp.snappy.interfaces.snapjob import ISnapRequestBuildsJobSource130from lp.snappy.interfaces.snapjob import ISnapRequestBuildsJobSource
131from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient131from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
@@ -137,6 +137,9 @@ from lp.snappy.model.snap import (
137from lp.snappy.model.snapbuild import SnapFile137from lp.snappy.model.snapbuild import SnapFile
138from lp.snappy.model.snapbuildjob import SnapBuildJob138from lp.snappy.model.snapbuildjob import SnapBuildJob
139from lp.snappy.model.snapjob import SnapJob139from lp.snappy.model.snapjob import SnapJob
140from lp.snappy.tests.test_snapbuildjob import (
141 FakeSnapStoreClient,
142 run_isolated_jobs)
140from lp.testing import (143from lp.testing import (
141 admin_logged_in,144 admin_logged_in,
142 ANONYMOUS,145 ANONYMOUS,
@@ -1079,6 +1082,45 @@ class TestSnap(TestCaseWithFactory):
1079 snap.destroySelf()1082 snap.destroySelf()
1080 self.assertFalse(getUtility(ISnapSet).exists(owner, "condemned"))1083 self.assertFalse(getUtility(ISnapSet).exists(owner, "condemned"))
10811084
1085 def test_getBuildByStoreRevision(self):
1086 snap1 = self.factory.makeSnap()
1087 build = self.factory.makeSnapBuild(
1088 snap=snap1,
1089 status=BuildStatus.FULLYBUILT)
1090 job = getUtility(ISnapStoreUploadJobSource).create(build)
1091 client = FakeSnapStoreClient()
1092 client.upload.result = (
1093 "http://sca.example/dev/api/snaps/1/builds/1/status")
1094 client.checkStatus.result = (
1095 "http://sca.example/dev/click-apps/1/rev/1/", 1)
1096 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
1097 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
1098 run_isolated_jobs([job])
1099 self.assertEqual(
1100 SnapBuildStoreUploadStatus.UPLOADED, build.store_upload_status)
1101 self.assertEqual(build.store_upload_revision, 1)
1102 self.assertEqual(snap1.getBuildByStoreRevision(1), build)
1103
1104 # build & upload again, check revision
1105 # and that we return the second build for revision 2
1106 build2 = self.factory.makeSnapBuild(
1107 snap=snap1,
1108 status=BuildStatus.FULLYBUILT)
1109 job = getUtility(ISnapStoreUploadJobSource).create(build2)
1110 client = FakeSnapStoreClient()
1111 client.upload.result = (
1112 "http://sca.example/dev/api/snaps/1/builds/2/status")
1113 client.checkStatus.result = (
1114 "http://sca.example/dev/click-apps/1/rev/2/", 2)
1115 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
1116 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
1117 run_isolated_jobs([job])
1118 self.assertEqual(
1119 SnapBuildStoreUploadStatus.UPLOADED, build2.store_upload_status)
1120 self.assertEqual(build2.store_upload_revision, 2)
1121 self.assertEqual(snap1.getBuildByStoreRevision(2), build2)
1122 self.assertEqual(snap1.getBuildByStoreRevision(1), build)
1123
1082 def test_getBuildSummariesForSnapBuildIds(self):1124 def test_getBuildSummariesForSnapBuildIds(self):
1083 snap1 = self.factory.makeSnap()1125 snap1 = self.factory.makeSnap()
1084 snap2 = self.factory.makeSnap()1126 snap2 = self.factory.makeSnap()

Subscribers

People subscribed via source and target branches

to status/vote changes: