Merge ~cjwatson/launchpad:db-bugsummary-statement-triggers into launchpad:db-devel

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 63fbb23e544d312b59caf14814b2c64f8948fb92
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:db-bugsummary-statement-triggers
Merge into: launchpad:db-devel
Diff against target: 256 lines (+226/-9)
2 files modified
database/schema/patch-2210-06-0.sql (+224/-0)
database/schema/security.cfg (+2/-9)
Reviewer Review Type Date Requested Status
William Grant db Approve
Ioana Lasc (community) Approve
Review via email: mp+373765@code.launchpad.net

Commit message

Rewrite BugSummary triggers to be statement-level

Description of the change

Rewrite the BugSummaryJournal maintenance triggers to make use of the new transition tables provided to AFTER ... FOR EACH STATEMENT triggers as of PostgreSQL 10. Instead of using row-level triggers which accumulate changes in a temporary table and flush it into the journal, we now write directly to the journal at the end of each statement.

There's a considerable amount of complexity here due to transition tables only being visible in the trigger function itself, not in functions that it calls. I used a combination of LATERAL and dynamic commands to minimise the amount of repeated code resulting from this. This does mean that the outermost part of each trigger function's query needs to be replanned each time the trigger runs, but I don't expect that to make a significant performance difference.

The transformations related to bug tags aren't completely obvious. I eliminated summarise_bug and unsummarise_bug from the call chain, as they didn't seem to be pulling their weight. When BugTag is changed, rather than decrementing/incrementing all the BugSummary rows that the changed bugs expand to, it now makes more sense to do so only for the rows relating to the changed tags. This necessitated extending bugsummary_journal_bugtaskflat and friends to take an array of tags, so it now processes all tags on the bug plus NULL when handling BugTaskFlat changes, and only the changed tags when handling BugTag changes.

I can't say for sure whether this will fix the periodic bug update timeouts we've been seeing, since we've never completely got to the bottom of their cause. However, reducing the number of times the trigger functions need to be called and eliminating their use of an explicit temporary table seem likely to improve matters.

This is essentially the same as https://code.launchpad.net/~cjwatson/launchpad/db-bugsummary-statement-triggers/+merge/371297, converted to git and rebased on master.

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

Looks good to me.

review: Approve
Revision history for this message
William Grant (wgrant) wrote :

During the review I wanted to see if a few changes in approach were viable, but ended up going further than expected and wound up with a solution that is structured a bit differently, and doesn't use any dynamic queries at all: https://paste.ubuntu.com/p/SQzFYMBBqZ/

Thoughts on that?

33f2227... by William Grant

Simplify BugSummary triggers further

This now doesn't use any dynamic queries at all.

[Posted by William to
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/373765;
tidied up slightly by Colin.]

Revision history for this message
Colin Watson (cjwatson) wrote :

After going through very carefully to make sure I understood it: I like it! I think the essential things I'd missed were the local types, the inline rewrite of bugsummary_tag_names using array_append, the trick of accumulating rows in a function-local array, and the trick of assigning to temp_journal.count to simplify fixing up the count in journal rows.

I tidied up your paste slightly (removing a couple of now-obsolete comments, and signing the XXX comments), and committed it. If you're happy with the result then we can probably go ahead?

77491fa... by Colin Watson

Update function declarations in security.cfg

63fbb23... by Colin Watson

Give todo_bugsummary_* slightly better names

Revision history for this message
William Grant (wgrant) :
review: Approve (db)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/database/schema/patch-2210-06-0.sql b/database/schema/patch-2210-06-0.sql
2new file mode 100644
3index 0000000..7a50ec7
4--- /dev/null
5+++ b/database/schema/patch-2210-06-0.sql
6@@ -0,0 +1,224 @@
7+-- Copyright 2019 Canonical Ltd. This software is licensed under the
8+-- GNU Affero General Public License version 3 (see the file LICENSE).
9+
10+SET client_min_messages=ERROR;
11+
12+-- Rewrite the BugSummaryJournal maintenance triggers to make use of the new
13+-- transition tables provided to AFTER ... FOR EACH STATEMENT triggers as of
14+-- PostgreSQL 10. Instead of using row-level triggers which accumulate
15+-- changes in a temporary table and flush it into the journal, we now write
16+-- directly to the journal at the end of each statement.
17+
18+DROP TRIGGER bugtaskflat_maintain_bug_summary ON bugtaskflat;
19+DROP TRIGGER bugtag_maintain_bug_summary_before_trigger ON bugtag;
20+DROP TRIGGER bugtag_maintain_bug_summary_after_trigger ON bugtag;
21+
22+-- Modify the existing bugsummary_locations to accept an array of tags
23+-- rather than looking them up for itself, and return journal rows rather than
24+-- bugsummary ones, since we haven't directly written to bugsummary in the
25+-- better part of a decade.
26+DROP FUNCTION bugsummary_locations;
27+CREATE FUNCTION bugsummary_locations(btf_row bugtaskflat, tags text[])
28+ RETURNS SETOF bugsummaryjournal
29+ LANGUAGE plpgsql
30+ AS $$
31+BEGIN
32+ IF btf_row.duplicateof IS NOT NULL THEN
33+ RETURN;
34+ END IF;
35+ RETURN QUERY
36+ SELECT
37+ CAST(NULL AS integer) AS id,
38+ CAST(1 AS integer) AS count,
39+ bug_targets.product, bug_targets.productseries,
40+ bug_targets.distribution, bug_targets.distroseries,
41+ bug_targets.sourcepackagename,
42+ bug_viewers.viewed_by, bug_tags.tag, btf_row.status,
43+ btf_row.milestone, btf_row.importance,
44+ btf_row.latest_patch_uploaded IS NOT NULL AS has_patch,
45+ bug_viewers.access_policy
46+ FROM
47+ bugsummary_targets(btf_row) AS bug_targets,
48+ unnest(tags) AS bug_tags (tag),
49+ bugsummary_viewers(btf_row) AS bug_viewers;
50+END;
51+$$;
52+
53+CREATE OR REPLACE FUNCTION bugsummary_insert_journals(
54+ journals bugsummaryjournal[])
55+ RETURNS void
56+ LANGUAGE sql
57+ AS $$
58+ -- We sum the rows here to minimise the number of inserts into the
59+ -- journal, as in the case of UPDATE statement we may have -1s and +1s
60+ -- cancelling each other out.
61+ INSERT INTO BugSummaryJournal(
62+ count, product, productseries, distribution,
63+ distroseries, sourcepackagename, viewed_by, tag,
64+ status, milestone, importance, has_patch, access_policy)
65+ SELECT
66+ SUM(count), product, productseries, distribution,
67+ distroseries, sourcepackagename, viewed_by, tag,
68+ status, milestone, importance, has_patch, access_policy
69+ FROM unnest(journals)
70+ GROUP BY
71+ product, productseries, distribution,
72+ distroseries, sourcepackagename, viewed_by, tag,
73+ status, milestone, importance, has_patch, access_policy
74+ HAVING SUM(count) != 0;
75+$$;
76+
77+-- Rewrite the existing bugtaskflat_maintain_bug_summary as a
78+-- statement-level trigger.
79+CREATE TYPE bugsummary_temp_btf_internal AS (
80+ btf bugtaskflat,
81+ count integer
82+);
83+
84+CREATE OR REPLACE FUNCTION bugtaskflat_maintain_bug_summary()
85+ RETURNS trigger
86+ LANGUAGE plpgsql SECURITY DEFINER
87+ SET search_path TO 'public'
88+ AS $$
89+DECLARE
90+ all_rows bugsummary_temp_btf_internal[];
91+ temp_rows bugsummary_temp_btf_internal[];
92+ journals bugsummaryjournal[];
93+ temp_rec record;
94+ temp_journal bugsummaryjournal;
95+BEGIN
96+ -- Work out the subqueries we need to compute the set of
97+ -- BugSummaryJournal rows that should be inserted.
98+ IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
99+ SELECT array_agg(row(old_bugtaskflat, -1))
100+ INTO STRICT temp_rows FROM old_bugtaskflat;
101+ all_rows := array_cat(all_rows, temp_rows);
102+ END IF;
103+ IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
104+ SELECT array_agg(row(new_bugtaskflat, 1))
105+ INTO STRICT temp_rows FROM new_bugtaskflat;
106+ all_rows := array_cat(all_rows, temp_rows);
107+ END IF;
108+
109+ -- XXX wgrant 2020-02-07: "The target is a record variable, row
110+ -- variable, or comma-separated list of scalar variables." but a list
111+ -- doesn't seem to work.
112+ FOR temp_rec IN
113+ SELECT journal, row.count
114+ FROM
115+ unnest(all_rows) AS row,
116+ LATERAL bugsummary_locations(
117+ row((row.btf).*),
118+ (SELECT array_append(array_agg(tag), NULL::text)
119+ FROM bugtag WHERE bug = (row.btf).bug)) AS journal
120+ LOOP
121+ temp_journal := temp_rec.journal;
122+ temp_journal.count := temp_rec.count;
123+ journals := array_append(journals, temp_journal);
124+ END LOOP;
125+
126+ PERFORM bugsummary_insert_journals(journals);
127+ RETURN NULL;
128+END;
129+$$;
130+
131+CREATE TRIGGER bugtaskflat_maintain_bug_summary_insert
132+ AFTER INSERT ON bugtaskflat
133+ REFERENCING NEW TABLE AS new_bugtaskflat
134+ FOR EACH STATEMENT EXECUTE PROCEDURE bugtaskflat_maintain_bug_summary();
135+
136+CREATE TRIGGER bugtaskflat_maintain_bug_summary_update
137+ AFTER UPDATE ON bugtaskflat
138+ REFERENCING OLD TABLE AS old_bugtaskflat NEW TABLE AS new_bugtaskflat
139+ FOR EACH STATEMENT EXECUTE PROCEDURE bugtaskflat_maintain_bug_summary();
140+
141+CREATE TRIGGER bugtaskflat_maintain_bug_summary_delete
142+ AFTER DELETE ON bugtaskflat
143+ REFERENCING OLD TABLE AS old_bugtaskflat
144+ FOR EACH STATEMENT EXECUTE PROCEDURE bugtaskflat_maintain_bug_summary();
145+
146+-- Rewrite the existing bugtag_maintain_bug_summary as a statement-level
147+-- trigger.
148+CREATE TYPE bugsummary_temp_bug_internal AS (
149+ bug integer,
150+ tags text[],
151+ count integer
152+);
153+
154+CREATE OR REPLACE FUNCTION bugtag_maintain_bug_summary()
155+ RETURNS trigger
156+ LANGUAGE plpgsql SECURITY DEFINER
157+ SET search_path TO 'public'
158+ AS $$
159+DECLARE
160+ all_rows bugsummary_temp_bug_internal[];
161+ temp_rows bugsummary_temp_bug_internal[];
162+ journals bugsummaryjournal[];
163+ temp_rec record;
164+ temp_journal bugsummaryjournal;
165+BEGIN
166+ -- Work out the subqueries we need to compute the set of
167+ -- BugSummaryJournal rows that should be inserted.
168+ IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
169+ SELECT array_agg(
170+ (SELECT row(bug, array_agg(tag), -1) FROM old_bugtag GROUP BY bug))
171+ INTO STRICT temp_rows;
172+ all_rows := array_cat(all_rows, temp_rows);
173+ END IF;
174+ IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
175+ SELECT array_agg(
176+ (SELECT row(bug, array_agg(tag), 1) FROM new_bugtag GROUP BY bug))
177+ INTO STRICT temp_rows;
178+ all_rows := array_cat(all_rows, temp_rows);
179+ END IF;
180+
181+ -- XXX wgrant 2020-02-07: "The target is a record variable, row
182+ -- variable, or comma-separated list of scalar variables." but a list
183+ -- doesn't seem to work.
184+ FOR temp_rec IN
185+ SELECT journal, row.count
186+ FROM
187+ unnest(all_rows) as row,
188+ LATERAL (
189+ SELECT journal.*
190+ FROM
191+ bugtaskflat btf,
192+ bugsummary_locations(btf, row.tags) AS journal
193+ WHERE btf.bug = row.bug
194+ ) AS journal
195+ LOOP
196+ temp_journal := temp_rec.journal;
197+ temp_journal.count := temp_rec.count;
198+ journals := array_append(journals, temp_journal);
199+ END LOOP;
200+
201+ PERFORM bugsummary_insert_journals(journals);
202+ RETURN NULL;
203+END;
204+$$;
205+
206+CREATE TRIGGER bugtag_maintain_bug_summary_insert
207+ AFTER INSERT ON bugtag
208+ REFERENCING NEW TABLE AS new_bugtag
209+ FOR EACH STATEMENT EXECUTE PROCEDURE bugtag_maintain_bug_summary();
210+
211+CREATE TRIGGER bugtag_maintain_bug_summary_update
212+ AFTER UPDATE ON bugtag
213+ REFERENCING OLD TABLE AS old_bugtag NEW TABLE AS new_bugtag
214+ FOR EACH STATEMENT EXECUTE PROCEDURE bugtag_maintain_bug_summary();
215+
216+CREATE TRIGGER bugtag_maintain_bug_summary_delete
217+ AFTER DELETE ON bugtag
218+ REFERENCING OLD TABLE AS old_bugtag
219+ FOR EACH STATEMENT EXECUTE PROCEDURE bugtag_maintain_bug_summary();
220+
221+DROP FUNCTION bugsummary_tags;
222+DROP FUNCTION bugsummary_journal_bug;
223+DROP FUNCTION bugsummary_journal_bugtaskflat;
224+DROP FUNCTION bug_row;
225+DROP FUNCTION bug_summary_flush_temp_journal;
226+DROP FUNCTION ensure_bugsummary_temp_journal;
227+DROP FUNCTION summarise_bug;
228+DROP FUNCTION unsummarise_bug;
229+
230+INSERT INTO LaunchpadDatabaseRevision VALUES (2210, 06, 0);
231diff --git a/database/schema/security.cfg b/database/schema/security.cfg
232index 8e4bbc9..5894f34 100644
233--- a/database/schema/security.cfg
234+++ b/database/schema/security.cfg
235@@ -86,19 +86,12 @@ public.valid_name(text) = EXECUTE
236 public.valid_regexp(text) = EXECUTE
237 public.version_sort_key(text) = EXECUTE
238 # BugSummary trigger functions and helpers.
239-public.bug_row(integer) =
240 public.bug_summary_dec(bugsummary) =
241-public.bug_summary_flush_temp_journal() =
242 public.bug_summary_inc(bugsummary) =
243-public.bugsummary_journal_bug(bug, integer) =
244-public.bugsummary_journal_bugtaskflat(bugtaskflat, integer) =
245-public.bugsummary_locations(bugtaskflat) =
246-public.bugsummary_tags(bugtaskflat) =
247+public.bugsummary_insert_journals(bugsummaryjournal[]) =
248+public.bugsummary_locations(bugtaskflat, text[]) =
249 public.bugsummary_targets(bugtaskflat) =
250 public.bugsummary_viewers(bugtaskflat) =
251-public.ensure_bugsummary_temp_journal() =
252-public.summarise_bug(integer) =
253-public.unsummarise_bug(integer) =
254
255 [ro]
256 groups=read

Subscribers

People subscribed via source and target branches

to status/vote changes: