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
1diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
2index 2445553..400b92e 100644
3--- a/lib/lp/snappy/interfaces/snap.py
4+++ b/lib/lp/snappy/interfaces/snap.py
5@@ -529,6 +529,21 @@ class ISnapView(Interface):
6
7 @call_with(user=REQUEST_USER)
8 @operation_parameters(
9+ store_upload_revision=Int(title="Store revision",
10+ required=True))
11+ @export_read_operation()
12+ @operation_for_version("devel")
13+ def getBuildByStoreRevision(store_upload_revision, user=None):
14+ """Returns the build (if any) of that snap recipe
15+ that has the given store_upload_revision.
16+
17+ :param store_upload_revision: A list of snap build request IDs.
18+ :param user: The `IPerson` requesting this information.
19+ :return: An 'ISnapBuild' or None.
20+ """
21+
22+ @call_with(user=REQUEST_USER)
23+ @operation_parameters(
24 request_ids=List(
25 title=_("A list of snap build request IDs."), value_type=Int(),
26 required=False),
27diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
28index 24aa2be..fa20509 100644
29--- a/lib/lp/snappy/model/snap.py
30+++ b/lib/lp/snappy/model/snap.py
31@@ -1111,6 +1111,15 @@ class Snap(Storm, WebhookTargetMixin):
32
33 return result
34
35+ def getBuildByStoreRevision(self, store_upload_revision):
36+ results = list(Store.of(self).find(
37+ SnapBuild,
38+ SnapBuild.snap == self))
39+ for build in results:
40+ if build.store_upload_revision == store_upload_revision:
41+ return build
42+ return None
43+
44 @property
45 def builds(self):
46 """See `ISnap`."""
47diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
48index 4d9c92f..b64ddc4 100644
49--- a/lib/lp/snappy/tests/test_snap.py
50+++ b/lib/lp/snappy/tests/test_snap.py
51@@ -124,8 +124,8 @@ from lp.snappy.interfaces.snapbase import (
52 )
53 from lp.snappy.interfaces.snapbuild import (
54 ISnapBuild,
55- ISnapBuildSet,
56- )
57+ ISnapBuildSet, SnapBuildStoreUploadStatus,
58+)
59 from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource
60 from lp.snappy.interfaces.snapjob import ISnapRequestBuildsJobSource
61 from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
62@@ -137,6 +137,9 @@ from lp.snappy.model.snap import (
63 from lp.snappy.model.snapbuild import SnapFile
64 from lp.snappy.model.snapbuildjob import SnapBuildJob
65 from lp.snappy.model.snapjob import SnapJob
66+from lp.snappy.tests.test_snapbuildjob import (
67+ FakeSnapStoreClient,
68+ run_isolated_jobs)
69 from lp.testing import (
70 admin_logged_in,
71 ANONYMOUS,
72@@ -1079,6 +1082,45 @@ class TestSnap(TestCaseWithFactory):
73 snap.destroySelf()
74 self.assertFalse(getUtility(ISnapSet).exists(owner, "condemned"))
75
76+ def test_getBuildByStoreRevision(self):
77+ snap1 = self.factory.makeSnap()
78+ build = self.factory.makeSnapBuild(
79+ snap=snap1,
80+ status=BuildStatus.FULLYBUILT)
81+ job = getUtility(ISnapStoreUploadJobSource).create(build)
82+ client = FakeSnapStoreClient()
83+ client.upload.result = (
84+ "http://sca.example/dev/api/snaps/1/builds/1/status")
85+ client.checkStatus.result = (
86+ "http://sca.example/dev/click-apps/1/rev/1/", 1)
87+ self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
88+ with dbuser(config.ISnapStoreUploadJobSource.dbuser):
89+ run_isolated_jobs([job])
90+ self.assertEqual(
91+ SnapBuildStoreUploadStatus.UPLOADED, build.store_upload_status)
92+ self.assertEqual(build.store_upload_revision, 1)
93+ self.assertEqual(snap1.getBuildByStoreRevision(1), build)
94+
95+ # build & upload again, check revision
96+ # and that we return the second build for revision 2
97+ build2 = self.factory.makeSnapBuild(
98+ snap=snap1,
99+ status=BuildStatus.FULLYBUILT)
100+ job = getUtility(ISnapStoreUploadJobSource).create(build2)
101+ client = FakeSnapStoreClient()
102+ client.upload.result = (
103+ "http://sca.example/dev/api/snaps/1/builds/2/status")
104+ client.checkStatus.result = (
105+ "http://sca.example/dev/click-apps/1/rev/2/", 2)
106+ self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
107+ with dbuser(config.ISnapStoreUploadJobSource.dbuser):
108+ run_isolated_jobs([job])
109+ self.assertEqual(
110+ SnapBuildStoreUploadStatus.UPLOADED, build2.store_upload_status)
111+ self.assertEqual(build2.store_upload_revision, 2)
112+ self.assertEqual(snap1.getBuildByStoreRevision(2), build2)
113+ self.assertEqual(snap1.getBuildByStoreRevision(1), build)
114+
115 def test_getBuildSummariesForSnapBuildIds(self):
116 snap1 = self.factory.makeSnap()
117 snap2 = self.factory.makeSnap()

Subscribers

People subscribed via source and target branches

to status/vote changes: