Merge lp:~stub/launchpad/db-deploy into lp:launchpad

Proposed by Stuart Bishop
Status: Merged
Approved by: Stuart Bishop
Approved revision: no longer in the source branch.
Merged at revision: 14433
Proposed branch: lp:~stub/launchpad/db-deploy
Merge into: lp:launchpad
Diff against target: 271 lines (+108/-51)
2 files modified
database/schema/security.cfg (+1/-1)
database/schema/upgrade.py (+107/-50)
To merge this branch: bzr merge lp:~stub/launchpad/db-deploy
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+84231@code.launchpad.net

Commit message

[r=adeuring][no-qa] Record bzr branch details when applying database updates

Description of the change

Record bzr branch details when applying database updates

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

Looks good.

+ def add_sql(sql):
+ sql = sql.strip()
+ if sql != '':
+ assert sql.endswith(';'), "SQL fragment not terminated with ';'"

 I'm wondering it if makes sense to add the statement(s)triggering the assert to the error message.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2011-11-28 20:07:20 +0000
3+++ database/schema/security.cfg 2011-12-03 04:54:33 +0000
4@@ -48,6 +48,7 @@
5 public.is_team(text) = EXECUTE
6 public.latestdatabasediskutilization =
7 public.launchpaddatabaserevision = SELECT
8+public.launchpaddatabaseupdatelog = SELECT
9 public.lp_account =
10 public.lp_openididentifier =
11 public.lp_person =
12@@ -1142,7 +1143,6 @@
13 public.karma = SELECT, INSERT, UPDATE
14 public.karmaaction = SELECT, INSERT, UPDATE
15 public.language = SELECT, INSERT, UPDATE
16-public.launchpaddatabaserevision = SELECT, INSERT, UPDATE
17 public.libraryfilealias = SELECT, INSERT
18 public.libraryfilecontent = SELECT, INSERT
19 public.logintoken = SELECT, INSERT, UPDATE
20
21=== modified file 'database/schema/upgrade.py'
22--- database/schema/upgrade.py 2011-10-10 14:58:41 +0000
23+++ database/schema/upgrade.py 2011-12-03 04:54:33 +0000
24@@ -19,9 +19,14 @@
25 import re
26 from tempfile import NamedTemporaryFile
27 from textwrap import dedent
28+import subprocess
29
30 from canonical.launchpad.scripts import db_options, logger_options, logger
31-from canonical.database.sqlbase import connect, ISOLATION_LEVEL_AUTOCOMMIT
32+from canonical.database.sqlbase import (
33+ connect,
34+ ISOLATION_LEVEL_AUTOCOMMIT,
35+ sqlvalues,
36+ )
37 from canonical.database.postgresql import fqn
38 import replication.helpers
39
40@@ -90,12 +95,26 @@
41 AND LaunchpadDatabaseRevision.start_time <> prev_end_time;
42
43 UPDATE LaunchpadDatabaseRevision
44- SET start_time=_start_time.start_time
45+ SET
46+ start_time=_start_time.start_time,
47+ branch_nick = %s,
48+ revno = %s,
49+ revid = %s
50 FROM _start_time
51 WHERE
52 LaunchpadDatabaseRevision.start_time
53 = transaction_timestamp() AT TIME ZONE 'UTC';
54 """)
55+START_UPDATE_LOG_SQL = dedent("""\
56+ INSERT INTO LaunchpadDatabaseUpdateLog (
57+ start_time, end_time, branch_nick, revno, revid)
58+ VALUES (transaction_timestamp() AT TIME ZONE 'UTC', NULL, %s, %s, %s);
59+ """)
60+FINISH_UPDATE_LOG_SQL = dedent("""\
61+ UPDATE LaunchpadDatabaseUpdateLog
62+ SET end_time = statement_timestamp() AT TIME ZONE 'UTC'
63+ WHERE start_time = transaction_timestamp() AT TIME ZONE 'UTC';
64+ """)
65
66
67 def to_seconds(td):
68@@ -148,12 +167,31 @@
69
70 def apply_patches_normal(con):
71 """Update a non replicated database."""
72+ # On dev environments, until we create a fresh database baseline the
73+ # LaunchpadDatabaseUpdateLog tables does not exist at this point (it
74+ # will be created later via database patch). Don't try to update
75+ # LaunchpadDatabaseUpdateLog if it does not exist.
76+ cur = con.cursor()
77+ cur.execute("""
78+ SELECT EXISTS (
79+ SELECT TRUE FROM information_schema.tables
80+ WHERE
81+ table_schema='public'
82+ AND table_name='launchpaddatabaseupdatelog')
83+ """)
84+ updatelog_exists = cur.fetchone()[0]
85+
86+ # Add a record to LaunchpadDatabaseUpdateLog that we are starting
87+ # an update.
88+ if updatelog_exists:
89+ cur.execute(START_UPDATE_LOG_SQL % sqlvalues(*get_bzr_details()))
90+
91 # trusted.sql contains all our stored procedures, which may
92 # be required for patches to apply correctly so must be run first.
93 apply_other(con, 'trusted.sql')
94
95 # Prepare to repair patch timestamps if necessary.
96- con.cursor().execute(FIX_PATCH_TIMES_PRE_SQL)
97+ cur.execute(FIX_PATCH_TIMES_PRE_SQL)
98
99 # Apply the patches
100 patches = get_patchlist(con)
101@@ -161,11 +199,17 @@
102 apply_patch(con, major, minor, patch, patch_file)
103
104 # Repair patch timestamps if necessary.
105- con.cursor().execute(FIX_PATCH_TIMES_POST_SQL)
106+ cur.execute(
107+ FIX_PATCH_TIMES_POST_SQL % sqlvalues(*get_bzr_details()))
108
109 # Update comments.
110 apply_comments(con)
111
112+ # Update the LaunchpadDatabaseUpdateLog record, stating the
113+ # completion time.
114+ if updatelog_exists:
115+ cur.execute(FINISH_UPDATE_LOG_SQL)
116+
117
118 def apply_patches_replicated():
119 """Update a Slony-I cluster."""
120@@ -181,73 +225,54 @@
121 log.info("Waiting for cluster to sync, pre-update.")
122 replication.helpers.sync(timeout=600)
123
124+ # Slonik script we are generating.
125 outf = StringIO()
126
127 # Start a transaction block.
128 print >> outf, "try {"
129
130- sql_to_run = []
131-
132- def run_sql(script):
133- if os.path.isabs(script):
134- full_path = script
135- else:
136- full_path = os.path.abspath(os.path.join(SCHEMA_DIR, script))
137- assert os.path.exists(full_path), "%s doesn't exist." % full_path
138- sql_to_run.append(full_path)
139-
140- # We are going to generate some temporary files using
141- # NamedTempoararyFile. Store them here so we can control when
142- # they get closed and cleaned up.
143- temporary_files = []
144+ # All the SQL we need to run, combined into one file. This minimizes
145+ # Slony-I syncs and downtime.
146+ combined_sql = NamedTemporaryFile(prefix='dbupdate', suffix='.sql')
147+
148+ def add_sql(sql):
149+ sql = sql.strip()
150+ if sql != '':
151+ assert sql.endswith(';'), "SQL not terminated with ';': %s" % sql
152+ print >> combined_sql, sql
153+ # Flush or we might lose statements from buffering.
154+ combined_sql.flush()
155+
156+ # Add a LaunchpadDatabaseUpdateLog record that we are starting patch
157+ # application.
158+ add_sql(START_UPDATE_LOG_SQL % sqlvalues(*get_bzr_details()))
159
160 # Apply trusted.sql
161- run_sql('trusted.sql')
162-
163- # We are going to generate some temporary files using
164- # NamedTempoararyFile. Store them here so we can control when
165- # they get closed and cleaned up.
166- temporary_files = []
167-
168- # Apply DB patches as one big hunk.
169- combined_script = NamedTemporaryFile(prefix='patch', suffix='.sql')
170- temporary_files.append(combined_script)
171+ add_sql(open(os.path.join(SCHEMA_DIR, 'trusted.sql'), 'r').read())
172
173 # Prepare to repair the start timestamps in
174 # LaunchpadDatabaseRevision.
175- print >> combined_script, FIX_PATCH_TIMES_PRE_SQL
176+ add_sql(FIX_PATCH_TIMES_PRE_SQL)
177
178 patches = get_patchlist(con)
179 for (major, minor, patch), patch_file in patches:
180- print >> combined_script, open(patch_file, 'r').read()
181+ add_sql(open(patch_file, 'r').read())
182
183 # Trigger a failure if the patch neglected to update
184 # LaunchpadDatabaseRevision.
185- print >> combined_script, (
186+ add_sql(
187 "SELECT assert_patch_applied(%d, %d, %d);"
188 % (major, minor, patch))
189
190 # Fix the start timestamps in LaunchpadDatabaseRevision.
191- print >> combined_script, FIX_PATCH_TIMES_POST_SQL
192-
193- combined_script.flush()
194- run_sql(combined_script.name)
195-
196- # Now combine all the written SQL (probably trusted.sql and
197- # patch*.sql) into one big file, which we execute with a single
198- # slonik execute_script statement to avoid multiple syncs.
199- single = NamedTemporaryFile(prefix='single', suffix='.sql')
200- for path in sql_to_run:
201- print >> single, open(path, 'r').read()
202- print >> single, ""
203- single.flush()
204+ add_sql(FIX_PATCH_TIMES_POST_SQL % sqlvalues(*get_bzr_details()))
205
206 print >> outf, dedent("""\
207 execute script (
208 set id = @lpmain_set, event node = @master_node,
209 filename='%s'
210 );
211- """ % single.name)
212+ """ % combined_sql.name)
213
214 # Close transaction block and abort on error.
215 print >> outf, dedent("""\
216@@ -265,11 +290,6 @@
217 raise SystemExit(4)
218 log.info("slonik(1) schema upgrade script completed.")
219
220- # Cleanup our temporary files - they applied successfully.
221- for temporary_file in temporary_files:
222- temporary_file.close()
223- del temporary_files
224-
225 # Wait for replication to sync.
226 log.info("Waiting for patches to apply to slaves and cluster to sync.")
227 replication.helpers.sync(timeout=0)
228@@ -598,6 +618,43 @@
229 apply_other(con, 'comments.sql')
230
231
232+_bzr_details_cache = None
233+
234+
235+def get_bzr_details():
236+ """Return (branch_nick, revno, revision_id) of this Bazaar branch.
237+
238+ Returns (None, None, None) if the tree this code is running from
239+ is not a Bazaar branch.
240+ """
241+ global _bzr_details_cache
242+ if _bzr_details_cache is None:
243+ cmd = [
244+ 'bzr',
245+ 'version-info',
246+ '--custom',
247+ '--template={branch_nick} {revno} {revision_id}',
248+ SCHEMA_DIR,
249+ ]
250+ p = subprocess.Popen(
251+ cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
252+ stderr=subprocess.PIPE)
253+ out, err = p.communicate()
254+
255+ if p.returncode == 0:
256+ branch_nick, revno, revision_id = out.split(' ', 3)
257+ log.debug("branch-nick: %s", branch_nick)
258+ log.debug("revno: %s", revno)
259+ log.debug("revision-id: %s", revision_id)
260+ else:
261+ log.error("Failed to retrieve Bazaar branch details")
262+ revision_id, revno, branch_nick = None, None, None
263+
264+ _bzr_details_cache = (branch_nick, revno, revision_id)
265+
266+ return _bzr_details_cache
267+
268+
269 if __name__ == '__main__':
270 parser = OptionParser()
271 db_options(parser)