~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to database/schema/upgrade.py

[rs=buildbot-poller] automatic merge from stable. Revisions: 14445,
        14446 included.

Show diffs side-by-side

added added

removed removed

Lines of Context:
20
20
from tempfile import NamedTemporaryFile
21
21
from textwrap import dedent
22
22
 
 
23
from bzrlib.branch import Branch
 
24
from bzrlib.errors import NotBranchError
 
25
 
23
26
from canonical.launchpad.scripts import db_options, logger_options, logger
24
 
from canonical.database.sqlbase import connect, ISOLATION_LEVEL_AUTOCOMMIT
 
27
from canonical.database.sqlbase import (
 
28
    connect,
 
29
    ISOLATION_LEVEL_AUTOCOMMIT,
 
30
    sqlvalues,
 
31
    )
25
32
from canonical.database.postgresql import fqn
26
33
import replication.helpers
27
34
 
90
97
        AND LaunchpadDatabaseRevision.start_time <> prev_end_time;
91
98
 
92
99
    UPDATE LaunchpadDatabaseRevision
93
 
    SET start_time=_start_time.start_time
 
100
    SET
 
101
        start_time=_start_time.start_time,
 
102
        branch_nick = %s,
 
103
        revno = %s,
 
104
        revid = %s
94
105
    FROM _start_time
95
106
    WHERE
96
107
        LaunchpadDatabaseRevision.start_time
97
108
            = transaction_timestamp() AT TIME ZONE 'UTC';
98
109
    """)
 
110
START_UPDATE_LOG_SQL = dedent("""\
 
111
    INSERT INTO LaunchpadDatabaseUpdateLog (
 
112
        start_time, end_time, branch_nick, revno, revid)
 
113
    VALUES (transaction_timestamp() AT TIME ZONE 'UTC', NULL, %s, %s, %s);
 
114
    """)
 
115
FINISH_UPDATE_LOG_SQL = dedent("""\
 
116
    UPDATE LaunchpadDatabaseUpdateLog
 
117
    SET end_time = statement_timestamp() AT TIME ZONE 'UTC'
 
118
    WHERE start_time = transaction_timestamp() AT TIME ZONE 'UTC';
 
119
    """)
99
120
 
100
121
 
101
122
def to_seconds(td):
148
169
 
149
170
def apply_patches_normal(con):
150
171
    """Update a non replicated database."""
 
172
    # On dev environments, until we create a fresh database baseline the
 
173
    # LaunchpadDatabaseUpdateLog tables does not exist at this point (it
 
174
    # will be created later via database patch). Don't try to update
 
175
    # LaunchpadDatabaseUpdateLog if it does not exist.
 
176
    cur = con.cursor()
 
177
    cur.execute("""
 
178
        SELECT EXISTS (
 
179
            SELECT TRUE FROM information_schema.tables
 
180
            WHERE
 
181
                table_schema='public'
 
182
                AND table_name='launchpaddatabaseupdatelog')
 
183
            """)
 
184
    updatelog_exists = cur.fetchone()[0]
 
185
 
 
186
    # Add a record to LaunchpadDatabaseUpdateLog that we are starting
 
187
    # an update.
 
188
    if updatelog_exists:
 
189
        cur.execute(START_UPDATE_LOG_SQL % sqlvalues(*get_bzr_details()))
 
190
 
151
191
    # trusted.sql contains all our stored procedures, which may
152
192
    # be required for patches to apply correctly so must be run first.
153
193
    apply_other(con, 'trusted.sql')
154
194
 
155
195
    # Prepare to repair patch timestamps if necessary.
156
 
    con.cursor().execute(FIX_PATCH_TIMES_PRE_SQL)
 
196
    cur.execute(FIX_PATCH_TIMES_PRE_SQL)
157
197
 
158
198
    # Apply the patches
159
199
    patches = get_patchlist(con)
161
201
        apply_patch(con, major, minor, patch, patch_file)
162
202
 
163
203
    # Repair patch timestamps if necessary.
164
 
    con.cursor().execute(FIX_PATCH_TIMES_POST_SQL)
 
204
    cur.execute(
 
205
        FIX_PATCH_TIMES_POST_SQL % sqlvalues(*get_bzr_details()))
165
206
 
166
207
    # Update comments.
167
208
    apply_comments(con)
168
209
 
 
210
    # Update the LaunchpadDatabaseUpdateLog record, stating the
 
211
    # completion time.
 
212
    if updatelog_exists:
 
213
        cur.execute(FINISH_UPDATE_LOG_SQL)
 
214
 
169
215
 
170
216
def apply_patches_replicated():
171
217
    """Update a Slony-I cluster."""
181
227
    log.info("Waiting for cluster to sync, pre-update.")
182
228
    replication.helpers.sync(timeout=600)
183
229
 
 
230
    # Slonik script we are generating.
184
231
    outf = StringIO()
185
232
 
186
233
    # Start a transaction block.
187
234
    print >> outf, "try {"
188
235
 
189
 
    sql_to_run = []
190
 
 
191
 
    def run_sql(script):
192
 
        if os.path.isabs(script):
193
 
            full_path = script
194
 
        else:
195
 
            full_path = os.path.abspath(os.path.join(SCHEMA_DIR, script))
196
 
        assert os.path.exists(full_path), "%s doesn't exist." % full_path
197
 
        sql_to_run.append(full_path)
198
 
 
199
 
    # We are going to generate some temporary files using
200
 
    # NamedTempoararyFile. Store them here so we can control when
201
 
    # they get closed and cleaned up.
202
 
    temporary_files = []
 
236
    # All the SQL we need to run, combined into one file. This minimizes
 
237
    # Slony-I syncs and downtime.
 
238
    combined_sql = NamedTemporaryFile(prefix='dbupdate', suffix='.sql')
 
239
 
 
240
    def add_sql(sql):
 
241
        sql = sql.strip()
 
242
        if sql != '':
 
243
            assert sql.endswith(';'), "SQL not terminated with ';': %s" % sql
 
244
            print >> combined_sql, sql
 
245
            # Flush or we might lose statements from buffering.
 
246
            combined_sql.flush()
 
247
 
 
248
    # Add a LaunchpadDatabaseUpdateLog record that we are starting patch
 
249
    # application.
 
250
    add_sql(START_UPDATE_LOG_SQL % sqlvalues(*get_bzr_details()))
203
251
 
204
252
    # Apply trusted.sql
205
 
    run_sql('trusted.sql')
206
 
 
207
 
    # We are going to generate some temporary files using
208
 
    # NamedTempoararyFile. Store them here so we can control when
209
 
    # they get closed and cleaned up.
210
 
    temporary_files = []
211
 
 
212
 
    # Apply DB patches as one big hunk.
213
 
    combined_script = NamedTemporaryFile(prefix='patch', suffix='.sql')
214
 
    temporary_files.append(combined_script)
 
253
    add_sql(open(os.path.join(SCHEMA_DIR, 'trusted.sql'), 'r').read())
215
254
 
216
255
    # Prepare to repair the start timestamps in
217
256
    # LaunchpadDatabaseRevision.
218
 
    print >> combined_script, FIX_PATCH_TIMES_PRE_SQL
 
257
    add_sql(FIX_PATCH_TIMES_PRE_SQL)
219
258
 
220
259
    patches = get_patchlist(con)
221
260
    for (major, minor, patch), patch_file in patches:
222
 
        print >> combined_script, open(patch_file, 'r').read()
 
261
        add_sql(open(patch_file, 'r').read())
223
262
 
224
263
        # Trigger a failure if the patch neglected to update
225
264
        # LaunchpadDatabaseRevision.
226
 
        print >> combined_script, (
 
265
        add_sql(
227
266
            "SELECT assert_patch_applied(%d, %d, %d);"
228
267
            % (major, minor, patch))
229
268
 
230
269
    # Fix the start timestamps in LaunchpadDatabaseRevision.
231
 
    print >> combined_script, FIX_PATCH_TIMES_POST_SQL
232
 
 
233
 
    combined_script.flush()
234
 
    run_sql(combined_script.name)
235
 
 
236
 
    # Now combine all the written SQL (probably trusted.sql and
237
 
    # patch*.sql) into one big file, which we execute with a single
238
 
    # slonik execute_script statement to avoid multiple syncs.
239
 
    single = NamedTemporaryFile(prefix='single', suffix='.sql')
240
 
    for path in sql_to_run:
241
 
        print >> single, open(path, 'r').read()
242
 
        print >> single, ""
243
 
    single.flush()
 
270
    add_sql(FIX_PATCH_TIMES_POST_SQL % sqlvalues(*get_bzr_details()))
244
271
 
245
272
    print >> outf, dedent("""\
246
273
        execute script (
247
274
            set id = @lpmain_set, event node = @master_node,
248
275
            filename='%s'
249
276
            );
250
 
        """ % single.name)
 
277
        """ % combined_sql.name)
251
278
 
252
279
    # Close transaction block and abort on error.
253
280
    print >> outf, dedent("""\
265
292
        raise SystemExit(4)
266
293
    log.info("slonik(1) schema upgrade script completed.")
267
294
 
268
 
    # Cleanup our temporary files - they applied successfully.
269
 
    for temporary_file in temporary_files:
270
 
        temporary_file.close()
271
 
    del temporary_files
272
 
 
273
295
    # Wait for replication to sync.
274
296
    log.info("Waiting for patches to apply to slaves and cluster to sync.")
275
297
    replication.helpers.sync(timeout=0)
598
620
    apply_other(con, 'comments.sql')
599
621
 
600
622
 
 
623
_bzr_details_cache = None
 
624
 
 
625
 
 
626
def get_bzr_details():
 
627
    """Return (branch_nick, revno, revision_id) of this Bazaar branch.
 
628
 
 
629
    Returns (None, None, None) if the tree this code is running from
 
630
    is not a Bazaar branch.
 
631
    """
 
632
    global _bzr_details_cache
 
633
    if _bzr_details_cache is None:
 
634
        try:
 
635
            branch = Branch.open_containing(SCHEMA_DIR)[0]
 
636
            revno, revision_id = branch.last_revision_info()
 
637
            branch_nick = branch.get_config().get_nickname()
 
638
        except NotBranchError:
 
639
            log.warning("Not a Bazaar branch - branch details unavailable")
 
640
            revision_id, revno, branch_nick = None, None, None
 
641
        _bzr_details_cache = (branch_nick, revno, revision_id)
 
642
    return _bzr_details_cache
 
643
 
 
644
 
601
645
if __name__ == '__main__':
602
646
    parser = OptionParser()
603
647
    db_options(parser)