~james-w/udd/storm-sqlite

162 by James Westby
Oops, actually add the icommon file.
1
#!/usr/bin/python
2
237.1.95 by James Westby
Add a way to set ws.size on the initial call.
3
import cgi
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
4
import datetime
237.1.9 by James Westby
Use fcntl locks as they expire with the processes.
5
import errno
6
import fcntl
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
7
import operator
162 by James Westby
Oops, actually add the icommon file.
8
import os
9
import random
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
10
import re
360 by James Westby
Produce a main.html and speed up email_failures.
11
import shutil
231 by James Westby
Fix iteration of large collections to not timeout LP.
12
import simplejson
237.1.2 by James Westby
Move to sqlite for the revid database.
13
import sqlite3
256 by James Westby
Add functions to replace MoM.
14
import StringIO
231 by James Westby
Fix iteration of large collections to not timeout LP.
15
import sys
162 by James Westby
Oops, actually add the icommon file.
16
import time
237.1.95 by James Westby
Add a way to set ws.size on the initial call.
17
import urllib
360 by James Westby
Produce a main.html and speed up email_failures.
18
import urllib2
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
19
import urlparse
162 by James Westby
Oops, actually add the icommon file.
20
209 by James Westby
Classify the revid handling.
21
from debian_bundle import changelog
22
256 by James Westby
Add functions to replace MoM.
23
from bzrlib import branch, diff, errors, merge, tag, testament, transport, ui
179.1.2 by Bazaar Package Importer
Log when sleeping due to an API issue.
24
from bzrlib.trace import mutter
25
162 by James Westby
Oops, actually add the icommon file.
26
232 by James Westby
Centralise the code to get a Launchpad and ensure everything uses the same root.
27
from launchpadlib.credentials import Credentials
28
from launchpadlib.errors import HTTPError
29
from launchpadlib.launchpad import Launchpad, EDGE_SERVICE_ROOT
30
31
269.1.2 by Bazaar Package Importer
Use production.
32
SERVICE_ROOT = EDGE_SERVICE_ROOT.replace("edge.", "")
232 by James Westby
Centralise the code to get a Launchpad and ensure everything uses the same root.
33
162 by James Westby
Oops, actually add the icommon file.
34
base_dir = "/srv/package-import.canonical.com/new/"
35
if not os.path.exists(base_dir):
36
    base_dir = "."
37
logs_dir = os.path.join(base_dir, "logs")
38
retry_dir = os.path.join(logs_dir, "retry")
172 by James Westby
Add a priority queue.
39
priority_dir = os.path.join(logs_dir, "priority")
162 by James Westby
Oops, actually add the icommon file.
40
lists_dir = os.path.join(base_dir, "lists")
41
revids_dir = os.path.join(base_dir, "revids")
42
lp_creds_file = os.path.join(base_dir, "lp_creds.txt")
43
last_time_file = os.path.join(base_dir, "last_run")
44
stop_file = os.path.join(base_dir, "STOP_PLEASE")
45
debug_log_file = os.path.join(base_dir, "debug_log")
46
progress_log_file = os.path.join(base_dir, "progress_log")
237.1.49 by James Westby
Make MAX_THREADS configurable at runtime.
47
max_threads_file = os.path.join(base_dir, "max_threads")
162 by James Westby
Oops, actually add the icommon file.
48
locks_dir = os.path.join(base_dir, "locks")
164 by James Westby
Add back updates_dir constant
49
updates_dir = os.path.join(base_dir, "updates")
237.1.2 by James Westby
Move to sqlite for the revid database.
50
sqlite_file = os.path.join(base_dir, "meta.db")
237.1.50 by James Westby
Move from a thread to list packges to the DB.
51
sqlite_package_file = os.path.join(base_dir, "packages.db")
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
52
sqlite_old_failures_file = os.path.join(base_dir, "failures.db")
366 by James Westby
Add a script to capture history of the number of failures and queue size.
53
sqlite_history_file = os.path.join(base_dir, "history.db")
358 by James Westby
Add a script to email new failures in main.
54
explanations_file = os.path.join(base_dir, "explanations")
237.1.2 by James Westby
Move to sqlite for the revid database.
55
sqlite_timeout = 30
269.1.1 by Bazaar Package Importer
Oops.
56
web_base_dir = logs_dir
256 by James Westby
Add functions to replace MoM.
57
debian_diffs_dir = os.path.join(web_base_dir, "diffs")
58
ubuntu_merge_dir = os.path.join(web_base_dir, "merges")
162 by James Westby
Oops, actually add the icommon file.
59
185 by James Westby
Add a script to categorise the current failures.
60
running_sentinel = "Apparently the supervisor died\n"
237.1.9 by James Westby
Use fcntl locks as they expire with the processes.
61
no_lock_returncode = 139
185 by James Westby
Add a script to categorise the current failures.
62
237.1.39 by James Westby
Add logging to try and see what sqlite is doing.
63
363.1.1 by Max Bowsher
Stop creating the unused packages_dir.
64
for directory in (logs_dir, retry_dir, priority_dir, lists_dir,
310.1.3 by John Arbash Meinel
Move the option parsing earlier
65
                  revids_dir, locks_dir, updates_dir,
310.1.1 by John Arbash Meinel
Add a lp_cache directory.
66
                  debian_diffs_dir, ubuntu_merge_dir,
67
                 ):
68
    if not os.path.exists(directory):
69
        os.mkdir(directory)
184 by James Westby
Create dirs if they don't exist.
70
181 by James Westby
Use an object to represent a version of a package needing to be imported.
71
urllib_launchpad_base_url = "https+urllib://launchpad.net/"
72
urllib_debian_base_url = "http+urllib://ftp.uk.debian.org/debian/"
73
urllib_debian_archive_base_url = "http+urllib://archive.debian.org/debian/"
74
331 by James Westby
Add maverick.
75
distro_releases = {"ubuntu": ["warty", "hoary", "breezy", "dapper", "edgy", "feisty", "gutsy", "hardy", "intrepid", "jaunty", "karmic", "lucid", "maverick"],
181 by James Westby
Use an object to represent a version of a package needing to be imported.
76
    "debian": ["woody", "sarge", "etch", "lenny", "squeeze", "sid", "experimental"],
77
}
78
79
distro_pockets = {"ubuntu": ["release", "security", "proposed", "updates", "backports"],
80
    "debian": ["release",],
81
}
82
283 by James Westby
Allow to set all other official branches.
83
# development releases first for stacking, no etch or earlier as they aren't on lp
331 by James Westby
Add maverick.
84
lp_distro_releases = {"ubuntu": ["maverick", "warty", "hoary", "breezy", "dapper", "edgy", "feisty", "gutsy", "hardy", "intrepid", "jaunty", "karmic", "lucid"],
283 by James Westby
Allow to set all other official branches.
85
    "debian": ["squeeze", "lenny", "sid", "experimental"],
86
}
87
256 by James Westby
Add functions to replace MoM.
88
default_debian_diff_release = "sid"
89
default_ubuntu_merge_source = "squeeze"
90
181 by James Westby
Use an object to represent a version of a package needing to be imported.
91
310.1.3 by John Arbash Meinel
Move the option parsing earlier
92
def get_lp(cache=None):
232 by James Westby
Centralise the code to get a Launchpad and ensure everything uses the same root.
93
    f = open(lp_creds_file, "rb")
94
    try:
95
        creds = Credentials("package-import")
96
        creds.load(f)
97
    finally:
98
        f.close()
310.1.3 by John Arbash Meinel
Move the option parsing earlier
99
    lp = Launchpad(creds, service_root=SERVICE_ROOT, cache=cache)
232 by James Westby
Centralise the code to get a Launchpad and ensure everything uses the same root.
100
    return lp
101
102
237.1.20 by James Westby
Handle .bzr in failures dir when migrating
103
def get_sqlite_connection(path):
237.1.31 by James Westby
Make datetime map to timestamp automatically
104
    conn = sqlite3.connect(path, timeout=sqlite_timeout,
105
            detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
237.1.20 by James Westby
Handle .bzr in failures dir when migrating
106
    conn.row_factory = sqlite3.Row
107
    return conn
108
109
181 by James Westby
Use an object to represent a version of a package needing to be imported.
110
def debian_base_url(release):
111
    if release in ["hamm", "slink", "potato", "woody", "sarge"]:
112
        return urllib_debian_archive_base_url
113
    return urllib_debian_base_url
114
162 by James Westby
Oops, actually add the icommon file.
115
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
116
def make_suite(release, pocket):
117
    if pocket == "release":
118
        suite = release
119
    elif pocket in ("security", "proposed", "updates", "backports"):
120
        suite = "%s-%s" % (release, pocket)
121
    else:
122
        assert False, "Unkown pocket: %s" % pocket
123
    return suite
124
125
237.1.9 by James Westby
Use fcntl locks as they expire with the processes.
126
def _lock_path(path):
127
    f = open(path, "wb")
128
    try:
129
        fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
237.1.13 by James Westby
Improve lock handling.
130
        return f
237.1.9 by James Westby
Use fcntl locks as they expire with the processes.
131
    except IOError, e:
132
        if e.errno in (errno.EACCES, errno.EAGAIN):
237.1.13 by James Westby
Improve lock handling.
133
            return None
237.1.9 by James Westby
Use fcntl locks as they expire with the processes.
134
        raise
135
136
def lock_package(package):
137
    path = os.path.join(locks_dir, package)
237.1.13 by James Westby
Improve lock handling.
138
    return _lock_path(path)
237.1.9 by James Westby
Use fcntl locks as they expire with the processes.
139
140
141
def lock_main():
142
    path = os.path.join(base_dir, "main_lock")
237.1.13 by James Westby
Improve lock handling.
143
    return _lock_path(path)
237.1.9 by James Westby
Use fcntl locks as they expire with the processes.
144
145
333 by James Westby
Add locking to cron jobs to prevent runaway problems when they are slow.
146
def lock_list_packages():
147
    path = os.path.join(base_dir, "list_packages")
148
    return _lock_path(path)
149
150
151
def lock_add_import_jobs():
152
    path = os.path.join(base_dir, "add_import_jobs")
153
    return _lock_path(path)
154
155
156
def lock_categorise_failures():
157
    path = os.path.join(base_dir, "categorise_failures")
158
    return _lock_path(path)
159
160
358 by James Westby
Add a script to email new failures in main.
161
def lock_email_failures():
162
    path = os.path.join(base_dir, "email_failures")
163
    return _lock_path(path)
164
165
356 by James Westby
Throttle back on checking archive.debian.org.
166
def lock_update_lists():
167
    path = os.path.join(base_dir, "update_lists")
168
    return _lock_path(path)
169
170
162 by James Westby
Oops, actually add the icommon file.
171
def lp_call(callable, *args, **kwargs):
168 by James Westby
Use exponential backoff starting from 1 second and rising to ~200.
172
    retries = 4
162 by James Westby
Oops, actually add the icommon file.
173
    retried = 0
174
    while retried < retries:
175
        try:
176
            return callable(*args, **kwargs)
177
        except HTTPError, e:
179.1.14 by Bazaar Package Importer
Fix an off-by-one that was stopping the exception being raised if we couldn't talk to LP.
178
            retried += 1
162 by James Westby
Oops, actually add the icommon file.
179
            if retried >= retries:
180
                raise
167 by James Westby
Only backoff on 5xx errors.
181
            if e.response.status // 100 == 5:
168 by James Westby
Use exponential backoff starting from 1 second and rising to ~200.
182
                # randomized exponential backoff
179.1.2 by Bazaar Package Importer
Log when sleeping due to an API issue.
183
                delay = random.randint(10,15)**(retried//2)
184
                mutter("Sleeping for %d due to %s from LP." % (delay, e))
185
                time.sleep(delay)
167 by James Westby
Only backoff on 5xx errors.
186
            else:
237.1.75 by James Westby
Work out what the redirected location is for mysterious 302s.
187
                if e.response.status == 302:
237.1.94 by James Westby
Fix format string.
188
                    print "Redirected to %s" % e.response.get("location")
237.1.79 by James Westby
When we get redirected to None print more info.
189
                    print "\n".join(["%s: %s" % pair for pair in e.response.items()])
190
                    print "\n-----\n%s\n----\n" % e.content
167 by James Westby
Only backoff on 5xx errors.
191
                raise
179.1.14 by Bazaar Package Importer
Fix an off-by-one that was stopping the exception being raised if we couldn't talk to LP.
192
    assert False, "Shouldn't get here"
181 by James Westby
Use an object to represent a version of a package needing to be imported.
193
194
195
class PackageToImport(object):
196
197
    def __init__(self, name, version, distro, release, pocket, component="",
198
            on_lp=True, url=None):
199
        self.name = name
200
        self.version = version
201
        self.distro = distro
202
        self.release = release
203
        self.component = component
204
        self.pocket = pocket
205
        self.on_lp = on_lp
206
        self.url = url
207
208
    def same_as(self, other):
209
        return (self.name == other.name
210
                and self.version == other.version
211
                and self.distro == other.distro
212
                and self.release == other.release
213
                and self.pocket == other.pocket)
214
215
    def get_url(self):
216
        if self.url is not None:
217
            return self.url
218
        if self.version.debian_version is not None:
219
            version_str = "%s-%s" % (self.version.upstream_version,
220
                    self.version.debian_version)
221
        else:
222
            version_str = self.version.upstream_version
223
        if self.distro == "ubuntu":
224
            files_url = urllib_launchpad_base_url + "ubuntu/%s/+source/%s/%s/+files/" % (self.release, self.name, str(self.version))
225
        else:
226
            if self.on_lp:
227
                files_url = urllib_launchpad_base_url + "debian/%s/+source/%s/%s/+files/" % (self.release, self.name, str(self.version))
228
            else:
229
                start = self.name[0]
230
                if self.name.startswith("lib"):
231
                    start = self.name[:4]
182 by James Westby
Allow extra Debian versions to be specified.
232
                files_url = debian_base_url(self.release) + "pool/%s/%s/%s/" % (self.component, start, self.name)
233
        self.url = files_url + "%s_%s.dsc" % (self.name, version_str)
181 by James Westby
Use an object to represent a version of a package needing to be imported.
234
        return self.url
235
236
    def __str__(self):
237
        return "PackageToImport(%s, %s, %s, %s, %s, %s)" % (self.name,
238
                self.version, self.distro, self.release, self.pocket,
239
                self.component)
240
241
242
class ImportList(object):
243
244
    def __init__(self):
245
        self.plist = []
246
247
    def sort(self):
248
        def compare_versions(a, b):
249
            if a.distro == "ubuntu" and a.distro == b.distro:
250
                if a.pocket == "backports" and b.pocket != "backports":
251
                    return -1
252
                if a.pocket != "backports" and b.pocket == "backports":
253
                    return 1
254
            if a.distro != b.distro:
255
                if a.distro == "debian":
256
                    return 1
257
                return -1
258
            if a.release != b.release:
259
                a_release = distro_releases[a.distro].index(a.release)
260
                b_release = distro_releases[b.distro].index(b.release)
261
                if a_release < b_release:
262
                    return 1
263
                return -1
264
            if a.pocket != b.pocket:
265
                a_pocket = distro_pockets[a.distro].index(a.pocket)
266
                b_pocket = distro_pockets[b.distro].index(b.pocket)
267
                if a_pocket < b_pocket:
268
                    return 1
269
                return -1
351 by James Westby
Fix the sorting order.
270
            if a.version < b.version:
271
                return 1
272
            if a.version > b.version:
273
                return -1
181 by James Westby
Use an object to represent a version of a package needing to be imported.
274
            return 0
275
        self.plist.sort(cmp=compare_versions)
276
277
    def reverse(self):
278
        self.plist.reverse()
279
280
    def add_if_needed(self, newp):
281
        for oldp in self.plist:
282
            if oldp.same_as(newp):
283
                return
284
        self.plist.append(newp)
182 by James Westby
Allow extra Debian versions to be specified.
285
286
    def __str__(self):
287
        return "[" + ", ".join(map(str, self.plist)) + "]"
288
209 by James Westby
Classify the revid handling.
289
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
290
def import_from_publications(status_db, publications, last_known_published):
291
    checked = set()
292
    newest_published = last_known_published
293
    for publication in publications:
277 by James Westby
Work with newer lplib.
294
        published_time = datetime.datetime.strptime(str(publication.date_created)[:19],
278 by James Westby
Actually support the new launchpadlib.
295
                   "%Y-%m-%d %H:%M:%S")
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
296
        name = publication.source_package_name
297
        checked.add(name)
298
        if (newest_published is None or published_time > newest_published):
299
            newest_published = published_time
237.1.6 by James Westby
Port requeue_package.py to sqlite
300
    status_db.add_import_jobs(checked, newest_published)
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
301
302
303
def create_import_jobs(lp, status_db):
304
    last_known_published = status_db.last_import_time()
305
    last_published_call = None
306
    if last_known_published is not None:
307
        last_known_published = \
308
                last_known_published - datetime.timedelta(0, 600)
309
        last_published_call = last_known_published.isoformat()
310
    # We want all the new Published records since the last run
311
    publications = set()
237.1.97 by James Westby
Fix calling convention for lp_call
312
    for p in iterate_collection(lp_call(call_with_limited_size,
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
313
            lp.distributions['ubuntu'].main_archive.getPublishedSources,
314
            status="Published",
237.1.97 by James Westby
Fix calling convention for lp_call
315
            created_since_date=last_published_call)):
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
316
        publications.add(p)
237.1.97 by James Westby
Fix calling convention for lp_call
317
    for p in iterate_collection(lp_call(call_with_limited_size,
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
318
            lp.distributions['debian'].main_archive.getPublishedSources,
319
            status="Pending",
237.1.97 by James Westby
Fix calling convention for lp_call
320
            created_since_date=last_published_call)):
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
321
        publications.add(p)
322
    import_from_publications(status_db, publications, last_known_published)
323
324
237.1.84 by James Westby
Add in auto-retry of issues.
325
class PackageStatus(object):
326
327
    def __init__(self, name):
328
        self.name = name
250 by James Westby
Show the number of failures.
329
        self.failure_count = 0
240 by James Westby
Objectify some of the status code, allowing the page to be richer.
330
        self.running = False
331
        self.queued = False
332
        self.auto_retry = False
333
        self.auto_retry_masked = False
334
        self.timestamp = None
335
        self.auto_retry_time = None
336
        self.signature = None
337
        self.raw_reason = None
237.1.84 by James Westby
Add in auto-retry of issues.
338
339
237.1.2 by James Westby
Move to sqlite for the revid database.
340
class StatusDatabase(object):
341
342
    FAILURES_TABLE = "failures"
343
    FAILURES_TABLE_CREATE = '''create table if not exists %s
344
                     (package text constraint nonull NOT NULL,
345
                      reason blob,
237.1.77 by James Westby
Add a time to the failure table so we can show newest failures.
346
                      when_failed timestamp,
358 by James Westby
Add a script to email new failures in main.
347
                      emailed integer default 0,
237.1.2 by James Westby
Move to sqlite for the revid database.
348
                      constraint isprimary PRIMARY KEY
349
                            (package))''' % FAILURES_TABLE
350
    FAILURES_TABLE_FIND = '''select * from %s where package=?''' % FAILURES_TABLE
237.1.6 by James Westby
Port requeue_package.py to sqlite
351
    FAILURES_TABLE_DELETE = '''delete from %s where package=?''' % FAILURES_TABLE
352
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
353
    OLD_FAILURES_TABLE = "old_failures"
354
    OLD_FAILURES_TABLE_CREATE = '''create table if not exists %s
355
                     (package text constraint nonull NOT NULL,
356
                      reason blob,
252 by James Westby
Embrace the extra column
357
                      when_failed timestamp,
246 by James Westby
Auto-retry multiple times.
358
                      last_failed timestamp,
247 by James Westby
Why do I always pick reserved column names?
359
                      failure_count integer,
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
360
                      constraint isprimary PRIMARY KEY
361
                            (package))''' % OLD_FAILURES_TABLE
237.1.88 by James Westby
Fix the autoretry code.
362
    OLD_FAILURES_TABLE_FIND = '''select * from %s where package=?''' % OLD_FAILURES_TABLE
363
    OLD_FAILURES_TABLE_DELETE = '''delete from %s where package=?''' % OLD_FAILURES_TABLE
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
364
237.1.3 by James Westby
Move over to sqlite for storing failures, and also move towards jobs.
365
    JOBS_TABLE = "jobs"
366
    JOBS_TABLE_CREATE = '''create table if not exists %s
367
                    (id integer primary key asc,
368
                     package text constraint nonull NOT NULL,
369
                     active integer constraint nonull NOT NULL,
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
370
                     type integer constraint nonull NOT NULL,
237.1.3 by James Westby
Move over to sqlite for storing failures, and also move towards jobs.
371
                     date_requested timestamp constraint nonull NOT NULL,
372
                     date_started timestamp,
373
                     date_completed timestamp)''' % JOBS_TABLE
374
    JOBS_TABLE_FIND = '''select * from %s where package=?
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
375
                    and active=? order by type desc''' % JOBS_TABLE
376
    JOBS_TABLE_NEXT = '''select * from %s where active=1
377
                    order by type desc''' % JOBS_TABLE
237.1.3 by James Westby
Move over to sqlite for storing failures, and also move towards jobs.
378
    JOBS_TABLE_SELECT_JOB = '''select * from %s where id=?''' % JOBS_TABLE
379
    JOBS_TABLE_INSERT = '''insert into %s values (?, ?, ?, ?, ?, ?, ?)''' % JOBS_TABLE
237.1.34 by James Westby
Use update rather than insert to change rows.
380
    JOBS_TABLE_UPDATE = '''update %s set package=?, active=?, type=?,
381
                    date_requested=?, date_started=?, date_completed=?
382
                    where id=?''' % JOBS_TABLE
237.1.2 by James Westby
Move to sqlite for the revid database.
383
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
384
    IMPORT_TABLE = "import"
385
    IMPORT_TABLE_CREATE = '''create table if not exists %s
386
                    (import timestamp constraint nonull NOT NULL)
387
                    ''' % IMPORT_TABLE
388
237.1.84 by James Westby
Add in auto-retry of issues.
389
    RETRY_TABLE = "should_retry"
390
    RETRY_TABLE_CREATE = '''create table if not exists %s
391
                    (signature blob constraint nonull NOT NULL,
392
                     constraint isprimary PRIMARY KEY
393
                            (signature))''' % RETRY_TABLE
394
    RETRY_TABLE_SELECT = '''select * from %s where signature=?''' % RETRY_TABLE
237.1.85 by James Westby
Add --auto to requeue package to signal that problem should be auto-retried.
395
    RETRY_TABLE_INSERT = '''insert into %s values (?)''' % RETRY_TABLE
237.1.84 by James Westby
Add in auto-retry of issues.
396
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
397
    JOB_TYPE_PRIORITY = 5
398
    JOB_TYPE_NEW = 3
399
    JOB_TYPE_RETRY = 1
400
237.1.84 by James Westby
Add in auto-retry of issues.
401
    AUTO_RETRY_SECONDS = 60*60*3
246 by James Westby
Auto-retry multiple times.
402
    MAX_AUTO_RETRY_COUNT = 5
237.1.84 by James Westby
Add in auto-retry of issues.
403
237.1.2 by James Westby
Move to sqlite for the revid database.
404
    def __init__(self, sqlite_path):
237.1.31 by James Westby
Make datetime map to timestamp automatically
405
        self.conn = get_sqlite_connection(sqlite_path)
237.1.2 by James Westby
Move to sqlite for the revid database.
406
        c = self.conn.cursor()
407
        c.execute(self.FAILURES_TABLE_CREATE)
237.1.84 by James Westby
Add in auto-retry of issues.
408
        c.execute(self.OLD_FAILURES_TABLE_CREATE)
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
409
        c.execute(self.JOBS_TABLE_CREATE)
410
        c.execute(self.IMPORT_TABLE_CREATE)
237.1.84 by James Westby
Add in auto-retry of issues.
411
        c.execute(self.RETRY_TABLE_CREATE)
237.1.2 by James Westby
Move to sqlite for the revid database.
412
        self.conn.commit()
413
        c.close()
414
237.1.50 by James Westby
Move from a thread to list packges to the DB.
415
    def _has_failed(self, c, package):
416
        rows = c.execute(self.FAILURES_TABLE_FIND, (package,)).fetchall()
417
        return (len(rows) > 0)
418
237.1.77 by James Westby
Add a time to the failure table so we can show newest failures.
419
    def _set_failure(self, c, package, reason, now):
237.1.50 by James Westby
Move from a thread to list packges to the DB.
420
        if self._has_failed(c, package):
237.1.77 by James Westby
Add a time to the failure table so we can show newest failures.
421
            c.execute('update %s set package=?, reason=?, when_failed=? '
422
                    'where package=?' % self.FAILURES_TABLE,
423
                    (package, reason, now, package))
237.1.3 by James Westby
Move over to sqlite for storing failures, and also move towards jobs.
424
        else:
237.1.77 by James Westby
Add a time to the failure table so we can show newest failures.
425
            c.execute('insert into %s values (?, ?, ?)'
426
                    % self.FAILURES_TABLE, (package, reason, now))
237.1.2 by James Westby
Move to sqlite for the revid database.
427
237.1.8 by James Westby
When mass_import.py starts it checks for runs that were interrupted and resumes them.
428
    def add_jobs_for_interrupted(self):
429
        c = self.conn.cursor()
430
        try:
431
            rows = c.execute('select * from %s where reason=?'
432
                    % self.FAILURES_TABLE, (running_sentinel,)).fetchall()
433
            for row in rows:
434
                c.execute(self.FAILURES_TABLE_DELETE, (row[0],))
435
                self._add_job(c, row[0], self.JOB_TYPE_PRIORITY)
436
            self.conn.commit()
437
        except:
438
            self.conn.rollback()
439
            raise
440
        finally:
441
            c.close()
442
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
443
    def _start_package(self, c, package):
444
        ret = None
445
        rows = c.execute(self.JOBS_TABLE_FIND, (package, 1)).fetchall()
237.1.91 by James Westby
Ensure that the timestamp isn't set to NULL when setting the failure.
446
        now = datetime.datetime.utcnow()
447
        first = now
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
448
        job_id = 0
449
        for row in rows:
237.1.34 by James Westby
Use update rather than insert to change rows.
450
            c.execute(self.JOBS_TABLE_UPDATE,
451
                    (row[1], 0, row[3], row[4], first, row[6], row[0]))
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
452
            first = None
453
            job_id = row[0]
237.1.50 by James Westby
Move from a thread to list packges to the DB.
454
        if self._has_failed(c, package):
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
455
            job_id = None
456
        else:
358 by James Westby
Add a script to email new failures in main.
457
            c.execute('insert into %s values (?, ?, ?, 0)' % self.FAILURES_TABLE,
237.1.91 by James Westby
Ensure that the timestamp isn't set to NULL when setting the failure.
458
                    (package, running_sentinel, now))
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
459
            ret = job_id
460
        return ret
237.1.3 by James Westby
Move over to sqlite for storing failures, and also move towards jobs.
461
237.1.50 by James Westby
Move from a thread to list packges to the DB.
462
    def start_package(self, package):
463
        c = self.conn.cursor()
464
        try:
465
            ret = self._start_package(c, package)
466
            self.conn.commit()
467
            return ret
468
        except:
469
            self.conn.rollback()
470
            raise
471
        finally:
472
            c.close()
473
237.1.3 by James Westby
Move over to sqlite for storing failures, and also move towards jobs.
474
    def finish_job(self, package, job_id, success, output):
475
        c = self.conn.cursor()
476
        try:
237.1.77 by James Westby
Add a time to the failure table so we can show newest failures.
477
            now = datetime.datetime.utcnow()
237.1.3 by James Westby
Move over to sqlite for storing failures, and also move towards jobs.
478
            if job_id > 0:
479
                row = c.execute(self.JOBS_TABLE_SELECT_JOB, (job_id,)).fetchone()
480
                if row is not None:
237.1.34 by James Westby
Use update rather than insert to change rows.
481
                    c.execute(self.JOBS_TABLE_UPDATE,
237.1.77 by James Westby
Add a time to the failure table so we can show newest failures.
482
                            (row[1], 0, row[3], row[4], row[5], now, row[0]))
237.1.3 by James Westby
Move over to sqlite for storing failures, and also move towards jobs.
483
            if success:
484
                row = c.execute(self.FAILURES_TABLE_FIND, (package,)).fetchone()
485
                if row is not None:
486
                    c.execute('delete from %s where package=?'
487
                            % self.JOBS_TABLE, (package,))
237.1.6 by James Westby
Port requeue_package.py to sqlite
488
                c.execute(self.FAILURES_TABLE_DELETE, (package,))
237.1.84 by James Westby
Add in auto-retry of issues.
489
                c.execute(self.OLD_FAILURES_TABLE_DELETE, (package,))
237.1.3 by James Westby
Move over to sqlite for storing failures, and also move towards jobs.
490
            else:
237.1.77 by James Westby
Add a time to the failure table so we can show newest failures.
491
                self._set_failure(c, package, output, now)
237.1.2 by James Westby
Move to sqlite for the revid database.
492
            self.conn.commit()
493
        except:
494
            self.conn.rollback()
237.1.6 by James Westby
Port requeue_package.py to sqlite
495
            raise
237.1.2 by James Westby
Move to sqlite for the revid database.
496
        finally:
497
            c.close()
498
237.1.6 by James Westby
Port requeue_package.py to sqlite
499
    def _add_job(self, c, package, job_type):
500
        c.execute(self.JOBS_TABLE_INSERT, (None, package, 1, job_type,
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
501
                    datetime.datetime.utcnow(), None, None))
502
237.1.6 by James Westby
Port requeue_package.py to sqlite
503
    def add_jobs(self, packages, job_type):
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
504
        c = self.conn.cursor()
505
        try:
506
            for package in packages:
237.1.6 by James Westby
Port requeue_package.py to sqlite
507
                self._add_job(c, package, job_type)
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
508
            self.conn.commit()
509
        except:
510
            self.conn.rollback()
237.1.6 by James Westby
Port requeue_package.py to sqlite
511
            raise
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
512
        finally:
513
            c.close()
514
515
    def last_import_time(self):
516
        c = self.conn.cursor()
517
        try:
237.1.30 by James Westby
Fix typo in execute.
518
            row = c.execute('select * from %s order by import desc'
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
519
                    % self.IMPORT_TABLE).fetchone()
520
            if row is not None:
521
                return row[0]
522
            return row
237.1.42 by James Westby
Fix database contention by adding missing .commit()
523
        finally:
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
524
            self.conn.rollback()
525
            c.close()
526
237.1.6 by James Westby
Port requeue_package.py to sqlite
527
    def add_import_jobs(self, packages, newest_published):
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
528
        c = self.conn.cursor()
529
        try:
530
            for package in packages:
237.1.6 by James Westby
Port requeue_package.py to sqlite
531
                self._add_job(c, package, self.JOB_TYPE_NEW)
237.1.30 by James Westby
Fix typo in execute.
532
            c.execute('insert into %s values (?)' % self.IMPORT_TABLE,
237.1.5 by James Westby
Move the publication job creation to functions to be run from cron.
533
                    (newest_published,))
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
534
            self.conn.commit()
535
        except:
536
            self.conn.rollback()
237.1.6 by James Westby
Port requeue_package.py to sqlite
537
            raise
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
538
        finally:
539
            c.close()
540
541
    def next_job(self):
542
        c = self.conn.cursor()
543
        try:
544
            job_id = 0
545
            package = None
237.1.37 by James Westby
Remove active jobs if the package has failed.
546
            found = False
547
            while not found:
548
                row = c.execute(self.JOBS_TABLE_NEXT).fetchone()
549
                if row is not None:
550
                    package = row[1]
551
                    job_id = self._start_package(c, package)
552
                    if job_id is None:
553
                        c.execute(self.JOBS_TABLE_UPDATE,
554
                                (row[1], 0, row[3], row[4], row[5],
555
                                 row[6], row[0]))
556
                    else:
557
                        found = True
558
                else:
559
                    found = True
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
560
                    job_id = 0
237.1.37 by James Westby
Remove active jobs if the package has failed.
561
                    package = None
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
562
            self.conn.commit()
563
            return (job_id, package)
564
        except:
565
            self.conn.rollback()
237.1.6 by James Westby
Port requeue_package.py to sqlite
566
            raise
237.1.4 by James Westby
Drive the work from the db, rather than ad-hoc queues.
567
        finally:
568
            c.close()
569
237.1.55 by James Westby
Add a script to show the failure reason for a package.
570
    def failure_reason(self, package):
571
        c = self.conn.cursor()
572
        try:
573
            row = c.execute(self.FAILURES_TABLE_FIND, (package,)).fetchone()
574
            if row is None:
575
                return row
576
            return row[1]
577
        finally:
578
            self.conn.rollback()
579
            c.close()
580
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
581
    def failure_signature(self, raw_reason):
582
        trace = raw_reason.splitlines()
583
        sig = ''
584
        if len(trace) == 1:
585
            if trace[0] == running_sentinel:
586
                return None
587
            # sometimes, Python exceptions do not have file references
588
            m = re.match('(\w+): ', trace[0])
589
            if m:
590
                return m.group(1)
591
            else:
592
                return trace[0].strip().replace("\n", " ")
593
        elif len(trace) < 3:
594
            return " ".join(trace).strip().replace("\n", " ")
595
596
        for l in trace:
597
            if l.startswith('  File'):
598
                sig += ':' + l.split()[-1]
599
600
        return trace[-1].split(':')[0].replace("\n", " ") + sig
601
298 by James Westby
Allow retrying all the packages that match the signature of one.
602
    def retry(self, package, force=False, priority=False, auto=False,
603
            all=False):
237.1.84 by James Westby
Add in auto-retry of issues.
604
        c = self.conn.cursor()
605
        try:
606
            row = c.execute(self.FAILURES_TABLE_FIND, (package,)).fetchone()
607
            if row is None:
608
                if not force:
609
                    return False
610
                job_type = self.JOB_TYPE_RETRY
611
                if priority:
612
                    job_type = self.JOB_TYPE_PRIORITY
613
                self._add_job(c, package, job_type)
614
            else:
615
                raw_reason = row[1]
269 by James Westby
Try and kill decode errors.
616
                raw_reason = raw_reason.encode("ascii", "replace")
237.1.84 by James Westby
Add in auto-retry of issues.
617
                sig = self.failure_signature(raw_reason)
246 by James Westby
Auto-retry multiple times.
618
                self._retry(c, package, sig, row[2], priority=priority)
298 by James Westby
Allow retrying all the packages that match the signature of one.
619
                if auto and sig != None:
237.1.88 by James Westby
Fix the autoretry code.
620
                    row = c.execute(self.RETRY_TABLE_SELECT, (sig,)).fetchone()
237.1.87 by James Westby
Get the logic the right way round.
621
                    if row is None:
237.1.86 by James Westby
Allow --auto to be used many times on the same signature.
622
                        c.execute(self.RETRY_TABLE_INSERT, (sig,))
298 by James Westby
Allow retrying all the packages that match the signature of one.
623
                if all and sig != None:
624
                    rows = c.execute('select * from %s'
625
                            % self.FAILURES_TABLE).fetchall()
626
                    for row in rows:
627
                        this_raw_reason = row[1].encode("ascii", "replace")
628
                        this_sig = self.failure_signature(raw_reason)
629
                        if this_sig == sig:
630
                            self._retry(c, row[0], sig, row[2],
631
                                    priority=priority)
237.1.84 by James Westby
Add in auto-retry of issues.
632
            self.conn.commit()
633
            return True
634
        except:
635
            self.conn.rollback()
636
            raise
637
        finally:
638
            c.close()
639
246 by James Westby
Auto-retry multiple times.
640
    def _retry(self, c, package, signature, timestamp, priority=False):
237.1.84 by James Westby
Add in auto-retry of issues.
641
        row = c.execute(self.OLD_FAILURES_TABLE_FIND, (package,)).fetchone()
642
        if row is not None:
246 by James Westby
Auto-retry multiple times.
643
            if signature == row[1]:
251 by James Westby
There's an unwanted column in old_failures.
644
                if row[4] > self.MAX_AUTO_RETRY_COUNT:
246 by James Westby
Auto-retry multiple times.
645
                    print ("Warning: %s has failed %d times in the same way"
251 by James Westby
There's an unwanted column in old_failures.
646
                            % (package, row[4]))
246 by James Westby
Auto-retry multiple times.
647
                c.execute('update %s set package=?, reason=?, '
252 by James Westby
Embrace the extra column
648
                        'when_failed=?, last_failed=?, failure_count=? '
649
                        'where package=?'
246 by James Westby
Auto-retry multiple times.
650
                        % self.OLD_FAILURES_TABLE,
252 by James Westby
Embrace the extra column
651
                        (package, signature, timestamp, timestamp, row[4]+1,
652
                         package))
246 by James Westby
Auto-retry multiple times.
653
            else:
255 by James Westby
Damn you SQL in strings.
654
                c.execute('update %s set package=?, reason=?, when_failed=?, '
247 by James Westby
Why do I always pick reserved column names?
655
                        'last_failed=?, failure_count=? where package=?'
246 by James Westby
Auto-retry multiple times.
656
                        % self.OLD_FAILURES_TABLE,
252 by James Westby
Embrace the extra column
657
                        (package, signature, timestamp, timestamp, 1, package))
246 by James Westby
Auto-retry multiple times.
658
        else:
252 by James Westby
Embrace the extra column
659
            c.execute('insert into %s values (?, ?, ?, ?, ?)'
237.1.84 by James Westby
Add in auto-retry of issues.
660
                    % self.OLD_FAILURES_TABLE,
252 by James Westby
Embrace the extra column
661
                    (package, signature, timestamp, timestamp, 1))
237.1.84 by James Westby
Add in auto-retry of issues.
662
        c.execute(self.FAILURES_TABLE_DELETE, (package,))
663
        job_type = self.JOB_TYPE_RETRY
664
        if priority:
665
            job_type = self.JOB_TYPE_PRIORITY
666
        self._add_job(c, package, job_type)
667
240 by James Westby
Objectify some of the status code, allowing the page to be richer.
668
    def _attempt_retry(self, c, info):
669
        row = c.execute(self.RETRY_TABLE_SELECT, (info.signature,)).fetchone()
237.1.84 by James Westby
Add in auto-retry of issues.
670
        if row is None:
671
            return False
240 by James Westby
Objectify some of the status code, allowing the page to be richer.
672
        info.auto_retry = True
673
        info.auto_retry_time = info.timestamp + datetime.timedelta(0,
674
                self.AUTO_RETRY_SECONDS)
675
        if info.auto_retry_time > datetime.datetime.utcnow():
676
            return False
677
        row = c.execute(self.OLD_FAILURES_TABLE_FIND, (info.name,)).fetchone()
678
        if row is not None and row[1] == info.signature:
251 by James Westby
There's an unwanted column in old_failures.
679
            info.failure_count += row[4]
680
            if row[4] > self.MAX_AUTO_RETRY_COUNT:
246 by James Westby
Auto-retry multiple times.
681
                info.auto_retry_masked = True
682
                return False
683
        self._retry(c, info.name, info.signature, info.timestamp)
237.1.84 by James Westby
Add in auto-retry of issues.
684
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
685
    def summarise_failures(self):
240 by James Westby
Objectify some of the status code, allowing the page to be richer.
686
        package_info = []
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
687
        c = self.conn.cursor()
688
        try:
689
            rows = c.execute('select * from %s'
690
                    % self.FAILURES_TABLE).fetchall()
691
            db_failures = [(row[0], row[1], row[2]) for row in rows]
240 by James Westby
Objectify some of the status code, allowing the page to be richer.
692
            for row in c.execute(self.JOBS_TABLE_NEXT).fetchall():
254 by James Westby
Properly exclude duplicates form the queued list.
693
                included = False
253 by James Westby
Don't expose duplicate jobs on the failures page.
694
                for info in package_info:
695
                    if info.name == row[1]:
254 by James Westby
Properly exclude duplicates form the queued list.
696
                        included = True
697
                if included:
698
                    continue
240 by James Westby
Objectify some of the status code, allowing the page to be richer.
699
                package_info.append(PackageStatus(row[1]))
700
                package_info[-1].queued = True
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
701
            reasons = {}
702
            for package, raw_reason, ts in db_failures:
240 by James Westby
Objectify some of the status code, allowing the page to be richer.
703
                package_info.append(PackageStatus(package))
243 by James Westby
Don't lose the if.
704
                if raw_reason == running_sentinel:
705
                    package_info[-1].running = True
258 by James Westby
Fix the name of the attribute.
706
                    package_info[-1].timestamp = ts
245 by James Westby
Break out of the loop if the packge is running
707
                    continue
269 by James Westby
Try and kill decode errors.
708
                raw_reason = raw_reason.encode("ascii", "replace")
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
709
                sig = self.failure_signature(raw_reason)
710
                if sig is None:
711
                    continue
712
                else:
243 by James Westby
Don't lose the if.
713
                    info = package_info[-1]
250 by James Westby
Show the number of failures.
714
                    info.failure_count = 1
240 by James Westby
Objectify some of the status code, allowing the page to be richer.
715
                    info.signature = sig
716
                    info.raw_reason = raw_reason
717
                    info.timestamp = ts
718
                    if self._attempt_retry(c, info):
237.1.84 by James Westby
Add in auto-retry of issues.
719
                        continue
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
720
                    reasons.setdefault(sig, [])
240 by James Westby
Objectify some of the status code, allowing the page to be richer.
721
                    reasons[sig].append(info)
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
722
            self.conn.commit()
240 by James Westby
Objectify some of the status code, allowing the page to be richer.
723
            return (reasons, package_info)
237.1.83 by James Westby
Move the analysis of failures to StatusDatabase
724
        except:
725
            self.conn.rollback()
726
            raise
727
        finally:
728
            c.close()
729
358 by James Westby
Add a script to email new failures in main.
730
    def unemailed_failures(self):
731
        c = self.conn.cursor()
732
        try:
733
            rows = c.execute('select * from %s where emailed=0'
734
                    % self.FAILURES_TABLE).fetchall()
735
            db_failures = [(row[0], row[1], row[2]) for row in rows]
736
            return db_failures
737
        except:
738
            self.conn.rollback()
739
            raise
740
        finally:
741
            c.close()
742
743
    def set_failures_emailed(self, failures):
744
        c = self.conn.cursor()
745
        try:
746
            for failure in failures:
362 by James Westby
Get the new scripts working.
747
                c.execute('update %s set emailed=1 where package=? '
748
                        '' % (self.FAILURES_TABLE),
749
                        (failure[0], ))
750
            self.conn.commit()
358 by James Westby
Add a script to email new failures in main.
751
        except:
752
            self.conn.rollback()
753
            raise
754
        finally:
755
            c.close()
756
237.1.2 by James Westby
Move to sqlite for the revid database.
757
758
class CommitDatabase(object):
759
237.1.14 by James Westby
"commit" is a reserved word in sqlite.
760
    COMMIT_TABLE = "commits"
237.1.2 by James Westby
Move to sqlite for the revid database.
761
    COMMIT_TABLE_CREATE = '''create table if not exists %s
762
                     (package text constraint nonull NOT NULL,
763
                      constraint isprimary PRIMARY KEY
764
                            (package))''' % COMMIT_TABLE
765
    COMMIT_TABLE_FIND = '''select * from %s where package=?''' % COMMIT_TABLE
766
    COMMIT_TABLE_INSERT = '''insert into %s values (?)''' % COMMIT_TABLE
767
    COMMIT_TABLE_DELETE = "delete from %s where package=?" % COMMIT_TABLE
768
769
    def __init__(self, sqlite_path, package):
770
        self.package = package
237.1.31 by James Westby
Make datetime map to timestamp automatically
771
        self.conn = get_sqlite_connection(sqlite_path)
237.1.2 by James Westby
Move to sqlite for the revid database.
772
        c = self.conn.cursor()
773
        c.execute(self.COMMIT_TABLE_CREATE)
774
        self.conn.commit()
775
        c.close()
776
777
    def has_commit_started(self):
778
        c = self.conn.cursor()
779
        try:
780
            rows = c.execute(self.COMMIT_TABLE_FIND, (self.package,)).fetchall()
237.1.44 by James Westby
Fix another off-by-one that was causing commits to be messed up
781
            return len(rows) > 0
237.1.42 by James Westby
Fix database contention by adding missing .commit()
782
        finally:
237.1.2 by James Westby
Move to sqlite for the revid database.
783
            self.conn.rollback()
784
            c.close()
785
786
    def start_commit(self):
787
        c = self.conn.cursor()
788
        try:
789
            rows = c.execute(self.COMMIT_TABLE_FIND, (self.package,)).fetchall()
790
            assert len(rows) == 0, "Already started commit"
791
            c.execute(self.COMMIT_TABLE_INSERT, (self.package,))
792
            self.conn.commit()
793
        except:
794
            self.conn.rollback()
237.1.6 by James Westby
Port requeue_package.py to sqlite
795
            raise
237.1.2 by James Westby
Move to sqlite for the revid database.
796
        finally:
797
            c.close()
798
799
    def finish_commit(self):
800
        c = self.conn.cursor()
801
        try:
802
            rows = c.execute(self.COMMIT_TABLE_FIND, (self.package,)).fetchall()
803
            assert len(rows) == 1, "Commit not started"
804
            c.execute(self.COMMIT_TABLE_DELETE, (self.package,))
805
            self.conn.commit()
806
        except:
807
            self.conn.rollback()
237.1.6 by James Westby
Port requeue_package.py to sqlite
808
            raise
237.1.2 by James Westby
Move to sqlite for the revid database.
809
        finally:
810
            c.close()
811
812
237.1.50 by James Westby
Move from a thread to list packges to the DB.
813
class PackageDatabase(object):
814
815
    PACKAGE_TABLE = "packages"
816
    PACKAGE_TABLE_CREATE = '''create table if not exists %s
817
                     (package text constraint nonull NOT NULL,
818
                      main integer constraint nonull NOT NULL,
819
                      constraint isprimary PRIMARY KEY
820
                            (package))''' % PACKAGE_TABLE
821
    PACKAGE_TABLE_FIND = '''select * from %s where package=?''' % PACKAGE_TABLE
822
    PACKAGE_TABLE_INSERT = '''insert into %s values (?, ?)''' % PACKAGE_TABLE
276 by James Westby
Add missing comma in SQL
823
    PACKAGE_TABLE_UPDATE = '''update %s set package=?, main=? where
237.1.50 by James Westby
Move from a thread to list packges to the DB.
824
                    package=?''' % PACKAGE_TABLE
825
    PACKAGE_TABLE_FIND_MAIN = '''select package from %s where main=1''' % PACKAGE_TABLE
826
    PACKAGE_TABLE_FIND_UNIVERSE = '''select package from %s where main=0''' % PACKAGE_TABLE
827
828
    UPDATE_TABLE = "packages_update"
829
    UPDATE_TABLE_CREATE = '''create table if not exists %s
237.1.53 by James Westby
update is also a reserved word in sqlite
830
                    (anupdate timestamp constraint nonull NOT NULL)
237.1.50 by James Westby
Move from a thread to list packges to the DB.
831
                    ''' % UPDATE_TABLE
832
833
    def __init__(self, sqlite_path):
834
        self.conn = get_sqlite_connection(sqlite_path)
835
        c = self.conn.cursor()
836
        c.execute(self.PACKAGE_TABLE_CREATE)
237.1.52 by James Westby
Create the package update table.
837
        c.execute(self.UPDATE_TABLE_CREATE)
237.1.50 by James Westby
Move from a thread to list packges to the DB.
838
        self.conn.commit()
839
        c.close()
840
841
    def last_update(self):
842
        c = self.conn.cursor()
843
        try:
237.1.54 by James Westby
Damn you strings for code.
844
            row = c.execute('select * from %s order by anupdate desc'
237.1.50 by James Westby
Move from a thread to list packges to the DB.
845
                    % self.UPDATE_TABLE).fetchone()
846
            if row is not None:
847
                return row[0]
848
            return row
849
        finally:
850
            self.conn.rollback()
851
            c.close()
852
853
    def update_packages(self, package_dict):
854
        c = self.conn.cursor()
855
        try:
856
            for (package, main) in package_dict.items():
267 by James Westby
Run the query before taking the len()
857
                if len(c.execute(self.PACKAGE_TABLE_FIND, (package,)
275 by James Westby
fetchall not findall
858
                            ).fetchall()) > 0:
237.1.50 by James Westby
Move from a thread to list packges to the DB.
859
                    c.execute(self.PACKAGE_TABLE_UPDATE,
860
                            (package, main, package))
861
                else:
862
                    c.execute(self.PACKAGE_TABLE_INSERT, (package, main))
863
            c.execute('insert into %s values (?)' % self.UPDATE_TABLE,
864
                    (datetime.datetime.utcnow(),))
865
            self.conn.commit()
866
        except:
867
            self.conn.rollback()
868
            raise
869
        finally:
870
            c.close()
871
360 by James Westby
Produce a main.html and speed up email_failures.
872
    def list_packages_in_main(self):
873
        c = self.conn.cursor()
874
        try:
875
            rows = c.execute(self.PACKAGE_TABLE_FIND_MAIN).fetchall()
876
            return set([row[0] for row in rows])
877
        finally:
878
            self.conn.rollback()
879
            c.close()
880
237.1.50 by James Westby
Move from a thread to list packges to the DB.
881
    def get_one(self, these_ones):
882
        c = self.conn.cursor()
883
        try:
884
            rows = c.execute(self.PACKAGE_TABLE_FIND_MAIN).fetchall()
885
            count = len(rows)
886
            for row in rows:
887
                if row[0] not in these_ones:
888
                    return row[0]
889
            rows = c.execute(self.PACKAGE_TABLE_FIND_UNIVERSE).fetchall()
890
            count += len(rows)
891
            if count == 0:
892
                raise StopIteration
893
            for row in rows:
894
                if row[0] not in these_ones:
895
                    return row[0]
896
        finally:
897
            self.conn.rollback()
898
            c.close()
899
900
209 by James Westby
Classify the revid handling.
901
class RevidDatabase(object):
902
237.1.2 by James Westby
Move to sqlite for the revid database.
903
    REVID_TABLE = "revids"
904
    REVID_TABLE_CREATE = '''create table if not exists %s
905
                     (package text constraint nonull NOT NULL,
906
                      version text constraint nonull NOT NULL,
907
                      suite text constraint nonull NOT NULL,
908
                      revid text constraint nonull NOT NULL,
909
                      testament text constraint nonull NOT NULL,
910
                      constraint isprimary PRIMARY KEY
911
                            (package, version, suite))'''
912
    REVID_TABLE_FIND = '''select * from %s where version=?
913
                          and package=? and suite=?'''
914
    REVID_TABLE_FIND_SUITE = '''select version from %s where package=?
915
                                and suite=?'''
916
    REVID_WORKING_TABLE = "revids_working"
917
    REVID_TABLE_FIND_PACKAGE = '''select * from %s where package=?'''
918
    REVID_TABLE_INSERT = "insert into %s values (?, ?, ?, ?, ?)"
919
    DELETE_WORKING = "delete from %s where package=?" % REVID_WORKING_TABLE
920
285 by James Westby
Add a way to store suffixes for urls for specific packages.
921
    SUFFIX_TABLE = "suffixes"
922
    SUFFIX_TABLE_CREATE = '''create table if not exists %s
923
                    (package text constraint nonull NOT NULL,
924
                     suffix text constraint nonull NOT NULL,
925
                     constraint isprimary PRIMARY KEY (package))''' % SUFFIX_TABLE
926
927
237.1.2 by James Westby
Move to sqlite for the revid database.
928
    def __init__(self, sqlite_path, package):
929
        self.package = package
237.1.31 by James Westby
Make datetime map to timestamp automatically
930
        self.conn = get_sqlite_connection(sqlite_path)
237.1.2 by James Westby
Move to sqlite for the revid database.
931
        c = self.conn.cursor()
932
        c.execute(self.REVID_TABLE_CREATE % self.REVID_TABLE)
933
        c.execute(self.REVID_TABLE_CREATE % self.REVID_WORKING_TABLE)
285 by James Westby
Add a way to store suffixes for urls for specific packages.
934
        c.execute(self.SUFFIX_TABLE_CREATE)
237.1.2 by James Westby
Move to sqlite for the revid database.
935
        self.conn.commit()
936
        c.close()
322 by James Westby
Keep the marks in memory until we're about to push.
937
        self.outstanding_marks = {}
237.1.2 by James Westby
Move to sqlite for the revid database.
938
327 by James Westby
Fix the tests to work with the new way the revid db works.
939
    def _memory_rows(self):
940
        return self.outstanding_marks.keys()
941
237.1.2 by James Westby
Move to sqlite for the revid database.
942
    def _working_rows(self):
943
        c = self.conn.cursor()
944
        try:
945
            return c.execute(self.REVID_TABLE_FIND_PACKAGE
946
                    % self.REVID_WORKING_TABLE, (self.package,)).fetchall()
237.1.42 by James Westby
Fix database contention by adding missing .commit()
947
        finally:
237.1.2 by James Westby
Move to sqlite for the revid database.
948
            self.conn.rollback()
949
            c.close()
950
951
    def _saved_rows(self):
952
        c = self.conn.cursor()
953
        try:
954
            return c.execute(self.REVID_TABLE_FIND_PACKAGE
955
                    % self.REVID_TABLE, (self.package,)).fetchall()
956
        finally:
237.1.42 by James Westby
Fix database contention by adding missing .commit()
957
            self.conn.rollback()
237.1.2 by James Westby
Move to sqlite for the revid database.
958
            c.close()
959
960
    def is_marked(self, version, suite):
961
        c = self.conn.cursor()
962
        try:
963
            rows = c.execute(self.REVID_TABLE_FIND % self.REVID_TABLE,
964
                    (str(version), self.package, suite)).fetchall()
965
            rows += c.execute(self.REVID_TABLE_FIND % self.REVID_WORKING_TABLE,
966
                    (str(version), self.package, suite)).fetchall()
324 by James Westby
Take account of the in-memory marks when querying the db.
967
            rows += [foo for foo in self.outstanding_marks
968
                        if foo == (str(version), suite)]
237.1.2 by James Westby
Move to sqlite for the revid database.
969
            assert len(rows) < 2, "Multiple versions for package/suite?"
970
            if len(rows) > 0:
971
                return True
972
            return False
237.1.42 by James Westby
Fix database contention by adding missing .commit()
973
        finally:
237.1.2 by James Westby
Move to sqlite for the revid database.
974
            self.conn.rollback()
975
            c.close()
976
977
    def last_marked_version(self, suite):
978
        c = self.conn.cursor()
979
        try:
237.1.48 by James Westby
Fix off-by-one caused by just getting the version field.
980
            versions = [a[0] for a in c.execute(self.REVID_TABLE_FIND_SUITE
237.1.2 by James Westby
Move to sqlite for the revid database.
981
                    % self.REVID_WORKING_TABLE,
237.1.47 by James Westby
Extract the version from the rows.
982
                    (self.package, suite)).fetchall()]
237.1.48 by James Westby
Fix off-by-one caused by just getting the version field.
983
            versions += [a[0] for a in c.execute(self.REVID_TABLE_FIND_SUITE
237.1.2 by James Westby
Move to sqlite for the revid database.
984
                    % self.REVID_TABLE,
237.1.47 by James Westby
Extract the version from the rows.
985
                    (self.package, suite)).fetchall()]
324 by James Westby
Take account of the in-memory marks when querying the db.
986
            versions += [str(foo[0]) for foo in self.outstanding_marks
987
                            if foo[1] == suite]
237.1.2 by James Westby
Move to sqlite for the revid database.
988
            if len(versions) > 0:
989
                versions.sort(key=lambda x: changelog.Version(x))
990
                return versions[-1]
991
            return None
237.1.42 by James Westby
Fix database contention by adding missing .commit()
992
        finally:
237.1.2 by James Westby
Move to sqlite for the revid database.
993
            self.conn.rollback()
994
            c.close()
995
996
    def check(self, version, revid, suite, branch):
237.1.41 by James Westby
Move the expensive bzr operations out of the db lock on check.
997
        branch.repository.lock_read()
998
        try:
999
            tment = testament.StrictTestament3.from_revision(
1000
                    branch.repository, revid)
1001
            sha = tment.as_sha1()
1002
        finally:
1003
            branch.repository.unlock()
237.1.2 by James Westby
Move to sqlite for the revid database.
1004
        c = self.conn.cursor()
1005
        try:
1006
            rows = c.execute(self.REVID_TABLE_FIND % self.REVID_TABLE,
1007
                    (str(version), self.package, suite)).fetchall()
1008
            rows += c.execute(self.REVID_TABLE_FIND % self.REVID_WORKING_TABLE,
1009
                    (str(version), self.package, suite)).fetchall()
324 by James Westby
Take account of the in-memory marks when querying the db.
1010
            if (version, suite) in self.outstanding_marks:
1011
                rows += dict(revid=self.outstanding_marks[(str(version), suite)][0],
1012
                        testament=self.outstanding_marks[(str(version), suite)][1])
237.1.2 by James Westby
Move to sqlite for the revid database.
1013
            if len(rows) < 1:
1014
                return False
1015
            assert len(rows) < 2, "Multiple versions for a package/suite?"
1016
            row = rows[0]
237.1.41 by James Westby
Move the expensive bzr operations out of the db lock on check.
1017
            if (revid, sha) != (row['revid'], row['testament']):
287 by James Westby
Don't mutter the assertion message.
1018
                assert False, ("%s != %s for %s %s, something has changed"
237.1.41 by James Westby
Move the expensive bzr operations out of the db lock on check.
1019
                     % (str((revid, sha)), str((row['revid'],
1020
                                 row['testament'])), self.package,
1021
                         str(version)))
1022
            return True
237.1.42 by James Westby
Fix database contention by adding missing .commit()
1023
        finally:
237.1.2 by James Westby
Move to sqlite for the revid database.
1024
            self.conn.rollback()
1025
            c.close()
1026
237.1.42 by James Westby
Fix database contention by adding missing .commit()
1027
    def _commit(self, c):
1028
        rows = c.execute(self.REVID_TABLE_FIND_PACKAGE
1029
                % self.REVID_WORKING_TABLE, (self.package,)).fetchall()
1030
        if len(rows) > 0:
237.1.2 by James Westby
Move to sqlite for the revid database.
1031
            def get_values():
1032
                for row in rows:
237.1.45 by James Westby
Row is annoying, so do it the long way.
1033
                    yield tuple([row[i] for i in range(5)])
237.1.2 by James Westby
Move to sqlite for the revid database.
1034
            c.executemany(self.REVID_TABLE_INSERT % self.REVID_TABLE,
1035
                    get_values())
1036
            c.execute(self.DELETE_WORKING, (self.package,))
237.1.42 by James Westby
Fix database contention by adding missing .commit()
1037
1038
    def commit(self):
1039
        c = self.conn.cursor()
1040
        try:
1041
            self._commit(c)
237.1.2 by James Westby
Move to sqlite for the revid database.
1042
            self.conn.commit()
1043
        except:
1044
            self.conn.rollback()
237.1.6 by James Westby
Port requeue_package.py to sqlite
1045
            raise
237.1.2 by James Westby
Move to sqlite for the revid database.
1046
        finally:
1047
            c.close()
1048
1049
    def cleanup_last_run(self, cleanup_cb, push_cb):
1050
        c = self.conn.cursor()
1051
        try:
1052
            rows = c.execute(self.REVID_TABLE_FIND_PACKAGE
1053
                    % self.REVID_WORKING_TABLE, (self.package,)).fetchall()
1054
            if len(rows) < 1:
1055
                mutter("Nothing to cleanup")
1056
                cleanup_cb()
1057
            else:
237.1.42 by James Westby
Fix database contention by adding missing .commit()
1058
                self.conn.rollback()
237.1.2 by James Westby
Move to sqlite for the revid database.
1059
                push_cb()
237.1.42 by James Westby
Fix database contention by adding missing .commit()
1060
                self._commit(c)
1061
                self.conn.commit()
237.1.2 by James Westby
Move to sqlite for the revid database.
1062
                cleanup_cb()
237.1.42 by James Westby
Fix database contention by adding missing .commit()
1063
        finally:
1064
            self.conn.rollback()
1065
            c.close()
1066
1067
    def discard_last_run(self):
1068
        c = self.conn.cursor()
1069
        try:
1070
            c.execute(self.DELETE_WORKING, (self.package,))
1071
            self.conn.commit()
237.1.2 by James Westby
Move to sqlite for the revid database.
1072
        except:
1073
            self.conn.rollback()
237.1.6 by James Westby
Port requeue_package.py to sqlite
1074
            raise
237.1.2 by James Westby
Move to sqlite for the revid database.
1075
        finally:
1076
            c.close()
1077
1078
    def mark(self, version, revid, suite, branch):
1079
        branch.lock_read()
1080
        try:
1081
            tment = testament.StrictTestament3.from_revision(branch.repository, revid)
1082
            sha = tment.as_sha1()
1083
        finally:
1084
            branch.unlock()
324 by James Westby
Take account of the in-memory marks when querying the db.
1085
        self.outstanding_marks[(str(version), suite)] = (revid, sha)
322 by James Westby
Keep the marks in memory until we're about to push.
1086
1087
    def commit_outstanding(self):
237.1.2 by James Westby
Move to sqlite for the revid database.
1088
        c = self.conn.cursor()
1089
        try:
322 by James Westby
Keep the marks in memory until we're about to push.
1090
            for version, suite in self.outstanding_marks:
1091
                revid, sha = self.outstanding_marks[(version, suite)]
1092
                rows = c.execute(self.REVID_TABLE_FIND % self.REVID_TABLE,
1093
                        (str(version), self.package, suite)).fetchall()
1094
                rows += c.execute(self.REVID_TABLE_FIND % self.REVID_WORKING_TABLE,
1095
                        (str(version), self.package, suite)).fetchall()
1096
                assert len(rows) < 1, "Trying to mark version %s in %s again" \
1097
                    % (str(version), suite)
1098
                c.execute(self.REVID_TABLE_INSERT % self.REVID_WORKING_TABLE,
1099
                        (self.package, str(version), suite, revid, sha))
327 by James Westby
Fix the tests to work with the new way the revid db works.
1100
            self.outstanding_marks = {}
237.1.2 by James Westby
Move to sqlite for the revid database.
1101
            self.conn.commit()
1102
        except:
1103
            self.conn.rollback()
237.1.6 by James Westby
Port requeue_package.py to sqlite
1104
            raise
237.1.2 by James Westby
Move to sqlite for the revid database.
1105
        finally:
1106
            c.close()
1107
285 by James Westby
Add a way to store suffixes for urls for specific packages.
1108
    def get_suffix(self):
1109
        c = self.conn.cursor()
1110
        try:
1111
            rows = c.execute("select * from %s where package=?"
286 by James Westby
Fix typo
1112
                    % self.SUFFIX_TABLE, (self.package,)).fetchall()
285 by James Westby
Add a way to store suffixes for urls for specific packages.
1113
            if len(rows) < 1:
1114
                return ""
1115
            return rows[0][1]
1116
        finally:
1117
            self.conn.rollback()
1118
            c.close()
1119
237.1.2 by James Westby
Move to sqlite for the revid database.
1120
366 by James Westby
Add a script to capture history of the number of failures and queue size.
1121
class HistoryDatabase(object):
1122
1123
    FAILED_TABLE = "failed"
1124
    FAILED_TABLE_CREATE = '''create table if not exists %s
1125
                     (attime timestamp constraint nonull NOT NULL,
1126
                      queue integer constraint nonull NOT NULL,
1127
                      failed integer constraint nonull NOT NULL,
1128
                      constraint isprimary PRIMARY KEY (attime))'''
1129
    MAIN_FAILED_TABLE = "failed"
1130
1131
    def __init__(self, sqlite_path):
1132
        self.conn = get_sqlite_connection(sqlite_path)
1133
        c = self.conn.cursor()
1134
        c.execute(self.FAILED_TABLE_CREATE % self.FAILED_TABLE)
1135
        c.execute(self.FAILED_TABLE_CREATE % self.MAIN_FAILED_TABLE)
1136
        self.conn.commit()
1137
        c.close()
1138
1139
    def _set_counts(self, table, queued, failed):
1140
        c = self.conn.cursor()
1141
        try:
1142
            rows = c.execute("insert into %s values (?, ?, ?)"
1143
                    % table,
1144
                    (datetime.datetime.utcnow(), queued, failed))
1145
            self.conn.commit()
1146
        except:
1147
            self.conn.rollback()
1148
            raise
1149
        finally:
1150
            c.close()
1151
1152
    def set_counts(self, queued, failed):
1153
        self._set_counts(self.FAILED_TABLE, queued, failed)
1154
1155
    def set_main_counts(self, queued, failed):
1156
        self._set_counts(self.MAIN_FAILED_TABLE, queued, failed)
1157
1158
237.1.2 by James Westby
Move to sqlite for the revid database.
1159
class MemoryRevidDatabase(RevidDatabase):
1160
209 by James Westby
Classify the revid handling.
1161
    def __init__(self, package):
1162
        self.package = package
237.1.2 by James Westby
Move to sqlite for the revid database.
1163
        self._maps = {}
209 by James Westby
Classify the revid handling.
1164
1165
    def is_marked(self, version, suite):
1166
        revid_map = self._get_map(suite)
1167
        return str(version) in revid_map
1168
1169
    def last_marked_version(self, suite):
1170
        revid_map = self._get_map(suite)
1171
        versions = revid_map.keys()
1172
        if len(versions) > 0:
1173
            versions.sort(key=lambda x: changelog.Version(x))
1174
            return versions[-1]
1175
        return None
1176
1177
    def check(self, version, revid, suite, branch):
1178
        revid_map = self._get_map(suite)
1179
        branch.repository.lock_read()
1180
        try:
1181
            tment = testament.StrictTestament3.from_revision(branch.repository, revid)
1182
            sha = tment.as_sha1()
1183
            if not str(version) in revid_map:
1184
                return False
1185
            if (revid, sha) != revid_map[str(version)]:
1186
                mutter("%s != %s for %s %s, something has changed"
1187
                     % (str((revid, sha)), str(revid_map[str(version)]), self.package,
1188
                                  str(version)))
1189
                return False
1190
            return True
1191
        finally:
1192
            branch.repository.unlock()
1193
237.1.2 by James Westby
Move to sqlite for the revid database.
1194
    def commit(self):
1195
        raise NotImplementedError(self.finish_commit)
209 by James Westby
Classify the revid handling.
1196
1197
    def cleanup_last_run(self, cleanup_cb, push_cb):
237.1.2 by James Westby
Move to sqlite for the revid database.
1198
        pass
1199
1200
    def discard_last_run(self):
1201
        pass
209 by James Westby
Classify the revid handling.
1202
1203
    def _get_map(self, suite):
237.1.2 by James Westby
Move to sqlite for the revid database.
1204
        self._maps.setdefault(suite, {})
1205
        return self._maps[suite]
1206
1207
    def _save_map(self, suite, map):
1208
        self._maps[suite] = map
209 by James Westby
Classify the revid handling.
1209
1210
    def mark(self, version, revid, suite, branch):
1211
        revid_map = self._get_map(suite)
1212
        branch.lock_read()
1213
        try:
1214
            tment = testament.StrictTestament3.from_revision(branch.repository, revid)
1215
            sha = tment.as_sha1()
1216
        finally:
1217
            branch.unlock()
1218
        assert str(version) not in revid_map, \
1219
              "Asked to mark %s %s %s, but it is already marked" \
1220
                    % (self.package, version, suite)
1221
        revid_map[str(version)] = (revid, sha)
214 by James Westby
Add an in-memory implementation of RevidDatabase
1222
        self._save_map(suite, revid_map)
1223
322 by James Westby
Keep the marks in memory until we're about to push.
1224
    def commit_outstanding(self):
1225
        pass
1226
214 by James Westby
Add an in-memory implementation of RevidDatabase
1227
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1228
class BranchStore(object):
1229
285 by James Westby
Add a way to store suffixes for urls for specific packages.
1230
    def __init__(self, package, lp, suffix):
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1231
        self.package = package
1232
        self.lp = lp
285 by James Westby
Add a way to store suffixes for urls for specific packages.
1233
        self.suffix = suffix
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1234
        self._series_cache = {}
1235
        self._sp_cache = {}
1236
231 by James Westby
Fix iteration of large collections to not timeout LP.
1237
    def _branch_location(self, distro, release, pocket, readonly=False):
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1238
        lp_branch = None
1239
        lp_series = self._get_lp_series(distro, release)
231 by James Westby
Fix iteration of large collections to not timeout LP.
1240
        scheme = "bzr+ssh://"
1241
        if readonly:
1242
            scheme = "http://"
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1243
        if lp_series is not None:
1244
            lp_sp = self._get_lp_source_package(distro, release)
1245
            lp_branch = lp_call(lp_sp.getBranch, pocket=self._lp_pocket(pocket))
1246
        if lp_branch is None:
1247
            distro, suite = self._translate_suite(distro, make_suite(release, pocket))
231 by James Westby
Fix iteration of large collections to not timeout LP.
1248
            return ("%sbazaar.launchpad.net/~ubuntu-branches/%s/"
285 by James Westby
Add a way to store suffixes for urls for specific packages.
1249
                    "%s/%s/%s%s" % (scheme, distro, release,
1250
                        self.package, suite, self.suffix))
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1251
        else:
329 by James Westby
Cope with a non-multi-version-aware launchpadlib.
1252
            path = urlparse.urlparse(lp_branch.self_link).path
330 by James Westby
Fix off-by-one.
1253
            lp_branch_path = path[path[1:].index('/')+1:]
231 by James Westby
Fix iteration of large collections to not timeout LP.
1254
            return "%sbazaar.launchpad.net%s" % (scheme, lp_branch_path)
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1255
1256
    def side_branch_location(self, distro, release, suite):
1257
        distro, suite = self._translate_suite(distro, suite)
1258
        now = datetime.datetime.utcnow()
1259
        now_str = now.strftime("%Y%m%d%H%M")
303 by James Westby
ubuntu-dev has no contact address so don't have them own the source branches yet.
1260
        return ("bzr+ssh://bazaar.launchpad.net/~ubuntu-branches/%s/"
227 by James Westby
Complete removal of username.
1261
                "%s/%s/%s" % (distro, release, self.package, suite + "-%s" % now_str))
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1262
1263
231 by James Westby
Fix iteration of large collections to not timeout LP.
1264
    def get_branch(self, importp, possible_transports=None, readonly=False):
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1265
        return self.get_branch_parts(importp.distro, importp.release, importp.pocket,
231 by James Westby
Fix iteration of large collections to not timeout LP.
1266
                possible_transports=possible_transports, readonly=readonly)
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1267
231 by James Westby
Fix iteration of large collections to not timeout LP.
1268
    def get_branch_parts(self, distro, release, pocket, possible_transports=None, readonly=False):
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1269
        try:
231 by James Westby
Fix iteration of large collections to not timeout LP.
1270
            return branch.Branch.open(self._branch_location(distro, release, pocket, readonly=readonly),
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1271
                    possible_transports=possible_transports)
1272
        except errors.NotBranchError:
1273
            return None
1274
1275
    def push_location(self, distro, release, pocket):
1276
        return self._branch_location(distro, release, pocket)
1277
1278
    def _set_status(self, distro, release, branch, _retry_count=0):
1279
        # series statuses are:
1280
        # Experimental
1281
        # Active Development
1282
        # Pre-release Freeze
1283
        # Current Stable Release
1284
        # Supported
1285
        # Obsolete
1286
        # Future
1287
        #
1288
        # branch statuses are:
1289
        # Experimental
1290
        # Development
1291
        # Mature
1292
        # Merged
1293
        # Abandoned
1294
        series = self._get_lp_series(distro, release)
1295
        if series.status in ('Experimental', 'Active Development',
1296
            'Pre-release Freeze', 'Future'):
1297
            branch.lifecycle_status = 'Development'
1298
        elif series.status in ('Current Stable Release', 'Supported'):
1299
            branch.lifecycle_status = 'Mature'
1300
        elif series.status in ('Obsolete',):
1301
            branch.lifecycle_status = 'Abandoned'
1302
        try:
1303
            lp_call(branch.lp_save)
1304
        except HTTPError, e:
1305
            if e.response.status == 412 and _retry_count < 3:
1306
                time.sleep(1)
1307
                self._set_status(distro, release, lp_call(self.lp.load, branch.self_link),
1308
                        _retry_count=_retry_count+1)
1309
            else:
1310
                raise
1311
293 by James Westby
Set the reviewer on new branches.
1312
    def _set_reviewer(self, branch, _retry_count=0):
1313
        ubuntu_dev = lp_call(operator.getitem, self.lp.people, 'ubuntu-dev')
1314
        branch.reviewer = ubuntu_dev
1315
        try:
1316
            lp_call(branch.lp_save)
1317
        except HTTPError, e:
1318
            if e.response.status == 412 and _retry_count < 3:
1319
                time.sleep(1)
1320
                self._set_reviewer(lp_call(self.lp.load, branch.self_link),
1321
                        _retry_count=_retry_count+1)
1322
            else:
1323
                raise
1324
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1325
    def _get_lp_source_package(self, distro, release):
1326
        if (distro, release) in self._sp_cache:
1327
            return self._sp_cache[(distro, release)]
1328
        lp_series = self._get_lp_series(distro, release)
1329
        lp_sp = lp_call(lp_series.getSourcePackage, name=self.package)
1330
        self._sp_cache[(distro, release)] = lp_sp
1331
        return lp_sp
1332
1333
    def _get_lp_series(self, distro, release):
1334
        if (distro, release) in self._series_cache:
1335
            return self._series_cache[(distro, release)]
220 by James Westby
Don't ask LP about series we know it doesn't have.
1336
        if distro == "debian" and release in ["woody", "sarge", "etch"]:
1337
            lp_series = None
1338
        else:
1339
            lp_distro = lp_call(operator.getitem, self.lp.distributions, distro)
1340
            lp_series = lp_call(lp_distro.getSeries, name_or_version=release)
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1341
        self._series_cache[(distro, release)] = lp_series
1342
        return lp_series
1343
1344
    def _lp_pocket(self, pocket):
1345
        return pocket[0].upper() + pocket[1:]
1346
1347
    def _translate_suite(self, distro, suite):
1348
        if distro == "debian":
1349
            if suite == "oldstable":
1350
                suite = "etch"
1351
            elif suite == "stable":
1352
                suite = "lenny"
1353
            elif suite == "testing":
1354
                suite = "squeeze"
1355
            elif suite == "unstable":
1356
                suite = "sid"
1357
        return distro, suite
1358
1359
    def _branch_url(self, distro, release, pocket):
1360
        lp_sp = self._get_lp_source_package(distro, release)
1361
        lp_branch = lp_call(lp_sp.getBranch, pocket=self._lp_pocket(pocket))
1362
        if lp_branch is None:
1363
            distro, suite = self._translate_suite(distro, make_suite(release, pocket))
329 by James Westby
Cope with a non-multi-version-aware launchpadlib.
1364
            return str(self.lp._root_uri) + '~ubuntu-branches/%s/%s/%s/%s' % (distro, release, self.package, suite)
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1365
        else:
1366
            return lp_branch.self_link
1367
293 by James Westby
Set the reviewer on new branches.
1368
    def set_official(self, distro, release, pocket, set_status=False,
1369
            set_reviewer=False):
213 by James Westby
Move to a class for LP branch locations so we can implement a dummy that ignores LP.
1370
        lp_sp = self._get_lp_source_package(distro, release)
1371
        branch_url = self._branch_url(distro, release, pocket)
1372
        lp_branch = lp_call(self.lp.load, branch_url)
1373
        lp_call(lp_sp.setBranch, branch=lp_branch,
1374
                pocket=self._lp_pocket(pocket))
1375
        if set_status:
1376
            self._set_status(distro, release, lp_branch)
293 by James Westby
Set the reviewer on new branches.
1377
        if set_reviewer:
1378
            self._set_reviewer(lp_branch)
215 by James Westby
Add an in-memory BranchStore
1379
281 by James Westby
Add a script to make a branch official.
1380
    def _set_official_direct(self, distro, release, pocket, branch_url):
1381
        lp_sp = self._get_lp_source_package(distro, release)
283 by James Westby
Allow to set all other official branches.
1382
        if branch_url is not None:
1383
            lp_branch = lp_call(self.lp.load, branch_url)
1384
        else:
1385
            lp_branch = None
281 by James Westby
Add a script to make a branch official.
1386
        lp_call(lp_sp.setBranch, branch=lp_branch,
1387
                pocket=self._lp_pocket(pocket))
1388
215 by James Westby
Add an in-memory BranchStore
1389
1390
class MemoryBranchStore(BranchStore):
1391
285 by James Westby
Add a way to store suffixes for urls for specific packages.
1392
    def __init__(self, package, lp, suffix):
215 by James Westby
Add an in-memory BranchStore
1393
        self.package = package
1394
1395
    def side_branch_location(self, distro, release, suite):
1396
        distro, suite = self._translate_suite(distro, suite)
1397
        now = datetime.datetime.utcnow()
1398
        now_str = now.strftime("%Y%m%d%H%M")
1399
        return os.path.join(updates_dir, self.package, suite + "-%s" % now_str)
1400
1401
236 by James Westby
Remember to update subclasses!
1402
    def get_branch_parts(self, distro, release, pocket,
1403
            possible_transports=None, readonly=False):
215 by James Westby
Add an in-memory BranchStore
1404
        return None
1405
1406
    def push_location(self, distro, release, pocket):
1407
        raise NotImplementedError(self.push_location)
1408
363.1.2 by Max Bowsher
Fix MemoryBranchStore.set_official prototype to match interface.
1409
    def set_official(self, distro, release, pocket, set_status=False,
1410
            set_reviewer=False):
215 by James Westby
Add an in-memory BranchStore
1411
        raise NotImplementedError(self.set_official)
231 by James Westby
Fix iteration of large collections to not timeout LP.
1412
1413
1414
def get_collection_slice(collection, start, stop, max_size):
1415
    existing_representation = collection._wadl_resource.representation
1416
    if (existing_representation is not None
1417
        and start < len(existing_representation['entries'])):
1418
        # An optimization: the first page of entries has already
1419
        # been loaded. This can happen if this collection is the
1420
        # return value of a named operation, or if the client did
1421
        # something like check the length of the collection.
1422
        #
1423
        # Either way, we've already made an HTTP request and
1424
        # gotten some entries back. The client has requested a
1425
        # slice that includes some of the entries we already have.
1426
        # In the best case, we can fulfil the slice immediately,
1427
        # without making another HTTP request.
1428
        #
1429
        # Even if we can't fulfil the entire slice, we can get one
1430
        # or more objects from the first page and then have fewer
1431
        # objects to retrieve from the server later. This saves us
1432
        # time and bandwidth, and it might let us save a whole
1433
        # HTTP request.
1434
        entry_page = existing_representation['entries']
1435
1436
        first_page_size = len(entry_page)
1437
        entry_dicts = entry_page[start:stop]
1438
        page_url = existing_representation.get('next_collection_link')
1439
    else:
1440
        # No part of this collection has been loaded yet, or the
1441
        # slice starts beyond the part that has been loaded. We'll
1442
        # use our secret knowledge of lazr.restful to set a value for
1443
        # the ws.start variable. That way we start reading entries
1444
        # from the first one we want.
1445
        first_page_size = max_size + 1
1446
        entry_dicts = []
1447
        page_url = collection._with_url_query_variable_set(
1448
            collection._wadl_resource.url, 'ws.start', start)
1449
1450
    desired_size = stop-start
1451
    more_needed = desired_size - len(entry_dicts)
1452
1453
    # Iterate over pages until we have the correct number of entries.
1454
    while more_needed > 0 and page_url is not None:
1455
        if (first_page_size is not None
1456
                and more_needed > 0 and more_needed < first_page_size):
1457
            # An optimization: it's likely that we need less than
1458
            # a full page of entries, because the number we need
1459
            # is less than the size of the first page we got.
1460
            # Instead of requesting a full-sized page, we'll
1461
            # request only the number of entries we think we'll
1462
            # need. If we're wrong, there's no problem; we'll just
1463
            # keep looping.
1464
            page_url = collection._with_url_query_variable_set(
1465
                page_url, 'ws.size', more_needed)
1466
        representation = simplejson.loads(
1467
            unicode(collection._root._browser.get(page_url)))
1468
        current_page_entries = representation['entries']
1469
        entry_dicts += current_page_entries[:more_needed]
1470
        more_needed = desired_size - len(entry_dicts)
1471
1472
        page_url = representation.get('next_collection_link')
1473
        if page_url is None:
1474
            # We've gotten the entire collection; there are no
1475
            # more entries.
1476
            break
1477
        if first_page_size is None:
1478
            first_page_size = len(current_page_entries)
1479
1480
1481
    # Convert entry_dicts into a list of Entry objects.
1482
    return [resource for resource
1483
            in collection._convert_dicts_to_entries(entry_dicts)]
1484
1485
237.1.96 by James Westby
Limit size of all calls to getPublishedSources
1486
def iterate_collection(collection, size=20):
231 by James Westby
Fix iteration of large collections to not timeout LP.
1487
    for i in xrange(0, sys.maxint, size):
1488
        slice = get_collection_slice(collection, i, i+size, size)
1489
        if len(slice) == 0:
1490
            break
1491
        for item in slice:
1492
            yield item
237.1.95 by James Westby
Add a way to set ws.size on the initial call.
1493
1494
237.1.96 by James Westby
Limit size of all calls to getPublishedSources
1495
def call_with_limited_size(operation, size=20, *args, **kwargs):
237.1.95 by James Westby
Add a way to set ws.size on the initial call.
1496
    """Invoke the method and process the result."""
1497
    if len(args) > 0:
1498
        raise TypeError('Method must be called with keyword args.')
1499
    http_method = operation.wadl_method.name
1500
    args = operation._transform_resources_to_links(kwargs)
1501
    request = operation.wadl_method.request
1502
1503
    def _with_url_query_variable_set(url, variable, new_value):
1504
        """A helper method to set a query variable in a URL."""
1505
        parts = urlparse.urlparse(url)
1506
        if parts.query is None:
1507
            params = {}
1508
        else:
1509
            params = cgi.parse_qs(parts.query)
1510
        params[variable] = str(new_value)
1511
        query = urllib.urlencode(params, True)
1512
        return urlparse.urlunparse((parts.scheme, parts.netloc, parts.path,
1513
                    parts.params, query, parts.fragment))
1514
1515
    for key, value in args.items():
1516
        # Certain parameter values should not be JSON-encoded:
1517
        # binary parameters (because they can't be JSON-encoded)
1518
        # and option values (because JSON-encoding them will screw
1519
        # up wadllib's parameter validation). The option value thing
1520
        # is a little hacky, but it's the best solution for now.
1521
        if not isinstance(value, basestring):
1522
            args[key] = simplejson.dumps(value)
1523
    if http_method in ('get', 'head', 'delete'):
1524
        url = operation.wadl_method.build_request_url(**args)
1525
        in_representation = ''
1526
        extra_headers = {}
1527
        url = _with_url_query_variable_set(url, 'ws.size', size)
1528
    else:
1529
        url = operation.wadl_method.build_request_url()
1530
        (media_type,
1531
         in_representation) = operation.wadl_method.build_representation(
1532
            **args)
1533
        extra_headers = { 'Content-type' : media_type }
1534
    response, content = operation.root._browser._request(
1535
        url, in_representation, http_method, extra_headers=extra_headers)
1536
1537
    if response.status == 201:
1538
        return operation._handle_201_response(url, response, content)
1539
    else:
1540
        if http_method == 'post':
1541
            # The method call probably modified this resource in
1542
            # an unknown way. Refresh its representation.
1543
            operation.resource.lp_refresh()
1544
        return operation._handle_200_response(url, response, content)
1545
256 by James Westby
Add functions to replace MoM.
1546
1547
def get_debian_ubuntu_branches(lp, temp_dir, package, bstore,
1548
        ensure_ubuntu_local=False, possible_transports=None):
1549
    try:
1550
        debian_b = branch.Branch.open(os.path.join(temp_dir,
1551
                    default_debian_diff_release))
1552
    except errors.NotBranchError:
1553
        debian_b = bstore.get_branch_parts("debian",
266 by James Westby
Pockets are lowercase when we are not talking to LP
1554
                default_debian_diff_release, "release",
256 by James Westby
Add functions to replace MoM.
1555
                possible_transports=possible_transports)
1556
    if debian_b is None:
1557
        # Not in unstable
1558
        return (None, None, None)
265 by James Westby
Ensure ubuntu_tree is always defined.
1559
    ubuntu_tree = None
256 by James Westby
Add functions to replace MoM.
1560
    ubuntu_current_series = lp.distributions['ubuntu'].current_series.name
1561
    try:
1562
        ubuntu_b = branch.Branch.open(os.path.join(temp_dir,
1563
                    ubuntu_current_series))
1564
        ubuntu_tree = ubuntu_b.bzrdir.open_workingtree()
1565
    except errors.NotBranchError:
1566
        ubuntu_b = bstore.get_branch_parts("ubuntu", ubuntu_current_series,
266 by James Westby
Pockets are lowercase when we are not talking to LP
1567
                "release", possible_transports=possible_transports)
256 by James Westby
Add functions to replace MoM.
1568
        if ubuntu_b is not None and ensure_ubuntu_local:
1569
            to_transport = transport.get_transport(os.path.join(temp_dir,
1570
                        ubuntu_current_series))
263 by James Westby
Don't overwrite the variable while we are holding the lock.
1571
            old_ubuntu_b = ubuntu_b
1572
            old_ubuntu_b.lock_read()
256 by James Westby
Add functions to replace MoM.
1573
            try:
263 by James Westby
Don't overwrite the variable while we are holding the lock.
1574
                revid = old_ubuntu_b.last_revision()
256 by James Westby
Add functions to replace MoM.
1575
                to_transport.mkdir('.')
310.1.1 by John Arbash Meinel
Add a lp_cache directory.
1576
                abzrdir = old_ubuntu_b.bzrdir.sprout(to_transport.base,
256 by James Westby
Add functions to replace MoM.
1577
                        revid, possible_transports=possible_transports)
310.1.1 by John Arbash Meinel
Add a lp_cache directory.
1578
                ubuntu_b = abzrdir.open_branch()
256 by James Westby
Add functions to replace MoM.
1579
                tag._merge_tags_if_possible(old_ubuntu_b, ubuntu_b)
310.1.1 by John Arbash Meinel
Add a lp_cache directory.
1580
                ubuntu_tree = abzrdir.open_workingtree()
256 by James Westby
Add functions to replace MoM.
1581
            finally:
263 by James Westby
Don't overwrite the variable while we are holding the lock.
1582
                old_ubuntu_b.unlock()
256 by James Westby
Add functions to replace MoM.
1583
    if ubuntu_b is None:
1584
        # Not in Ubuntu
1585
        return (None, None, None)
1586
    return (debian_b, ubuntu_b, ubuntu_tree)
1587
1588
1589
def generate_debian_diffs(lp, temp_dir, package, bstore,
1590
        possible_transports=None):
257 by James Westby
Remove any files if there is nothing to show and enable the diff generation
1591
    target_path = os.path.join(debian_diffs_dir, package)
256 by James Westby
Add functions to replace MoM.
1592
    debian_b, ubuntu_b, _ = get_debian_ubuntu_branches(lp, temp_dir, package,
1593
            bstore, possible_transports=possible_transports)
1594
    if debian_b is None:
257 by James Westby
Remove any files if there is nothing to show and enable the diff generation
1595
        if os.path.exists(target_path):
1596
            os.unlink(target_path)
256 by James Westby
Add functions to replace MoM.
1597
        return
1598
    debian_tree = debian_b.basis_tree()
1599
    ubuntu_tree = ubuntu_b.basis_tree()
1600
    out = StringIO.StringIO()
1601
    diff.show_diff_trees(debian_tree, ubuntu_tree, out, old_label='debian/',
1602
            new_label='ubuntu/')
257 by James Westby
Remove any files if there is nothing to show and enable the diff generation
1603
    if len(out.getvalue().strip()) > 0:
1604
        f = open(target_path, 'wb')
1605
        try:
1606
            f.write(out.getvalue())
1607
        finally:
1608
            f.close()
1609
    else:
1610
        if os.path.exists(target_path):
1611
            os.unlink(target_path)
256 by James Westby
Add functions to replace MoM.
1612
1613
1614
def possibly_generate_debian_diffs(lp, temp_dir, package, bstore,
1615
        possible_transports=None):
1616
    if os.path.exists(os.path.join(debian_diffs_dir, package)):
1617
        return
260 by James Westby
Fix the calling of the actual functions from the possibly ones.
1618
    generate_debian_diffs(lp, temp_dir, package, bstore,
256 by James Westby
Add functions to replace MoM.
1619
            possible_transports=possible_transports)
1620
1621
1622
def generate_ubuntu_merges(lp, temp_dir, package, bstore,
1623
        possible_transports=None):
1624
    debian_b, ubuntu_b, ubuntu_tree = get_debian_ubuntu_branches(lp,
1625
            temp_dir, package, bstore, ensure_ubuntu_local=True,
1626
            possible_transports=possible_transports)
257 by James Westby
Remove any files if there is nothing to show and enable the diff generation
1627
    target_path = os.path.join(ubuntu_merge_dir, package)
256 by James Westby
Add functions to replace MoM.
1628
    if debian_b is None:
257 by James Westby
Remove any files if there is nothing to show and enable the diff generation
1629
        if os.path.exists(target_path):
1630
            os.unlink(target_path)
256 by James Westby
Add functions to replace MoM.
1631
        return
1632
    debian_b.lock_read()
1633
    try:
259 by James Westby
You need a write lock to do a preview merge.
1634
        ubuntu_tree.lock_write()
256 by James Westby
Add functions to replace MoM.
1635
        try:
336 by James Westby
Handle (sort of) UnrelatedBranches when generating Ubuntu merges.
1636
            out = StringIO.StringIO()
256 by James Westby
Add functions to replace MoM.
1637
            debian_revid = debian_b.last_revision()
1638
            ubuntu_revid = ubuntu_b.last_revision()
1639
            pb = ui.ui_factory.nested_progress_bar()
336 by James Westby
Handle (sort of) UnrelatedBranches when generating Ubuntu merges.
1640
            try:
1641
                merger = merge.Merger.from_revision_ids(pb, ubuntu_tree,
1642
                        debian_revid, other_branch=debian_b)
1643
            except errors.UnrelatedBranches:
1644
                out.write("Unrelated branches!")
1645
                return
256 by James Westby
Add functions to replace MoM.
1646
            merger.merge_type = merge.Merge3Merger
1647
            if (merger.base_rev_id == merger.other_rev_id
1648
                    and merger.other_rev_id is not None):
257 by James Westby
Remove any files if there is nothing to show and enable the diff generation
1649
                if os.path.exists(target_path):
1650
                    os.unlink(target_path)
256 by James Westby
Add functions to replace MoM.
1651
                return
268 by James Westby
Don't generate merge patches where Ubuntu hasn't diverged.
1652
            if (merger.base_rev_id == ubuntu_revid):
1653
                if os.path.exists(target_path):
1654
                    os.unlink(target_path)
1655
                return
256 by James Westby
Add functions to replace MoM.
1656
            tree_merger = merger.make_merger()
1657
            tt = tree_merger.make_preview_transform()
1658
            try:
1659
                result_tree = tt.get_preview_tree()
1660
                delta = result_tree.changes_from(merger.this_tree)
1661
                delta.show(out)
1662
                if len(tree_merger.cooked_conflicts) > 0:
1663
                    out.write("conflicts:\n")
1664
                    for conflict in tree_merger.cooked_conflicts:
1665
                        out.write('  %s\n' % conflict)
1666
                out.write("\n\n")
1667
                # TODO: better labels
1668
                old_label = "ubuntu-current/"
1669
                new_label = "ubuntu-merged/"
1670
                out2 = StringIO.StringIO()
1671
                diff.show_diff_trees(merger.this_tree, result_tree, out2,
1672
                        old_label=old_label, new_label=new_label)
257 by James Westby
Remove any files if there is nothing to show and enable the diff generation
1673
                f = open(target_path, 'wb')
256 by James Westby
Add functions to replace MoM.
1674
                try:
1675
                    f.write(out.getvalue())
1676
                    f.write(out2.getvalue())
1677
                finally:
1678
                    f.close()
1679
            finally:
1680
                tt.finalize()
1681
        finally:
1682
            ubuntu_tree.unlock()
1683
    finally:
1684
        debian_b.unlock()
1685
1686
1687
def possibly_generate_ubuntu_merges(lp, temp_dir, package, bstore,
1688
        possible_transports=None):
1689
    if os.path.exists(os.path.join(ubuntu_merge_dir, package)):
1690
        return
260 by James Westby
Fix the calling of the actual functions from the possibly ones.
1691
    generate_ubuntu_merges(lp, temp_dir, package, bstore,
256 by James Westby
Add functions to replace MoM.
1692
            possible_transports=possible_transports)
328 by James Westby
Check whether the uploader forgot to tag before conflicting.
1693
1694
1695
def find_earliest_merge(b, revid):
1696
    for (stop_revid, depth, revno, end_of_merge) in b.iter_merge_sorted_revisions(
1697
                start_revision_id=b.last_revision(),
1698
                stop_revision_id=revid, stop_rule='include',
1699
                direction='forward'):
1700
        if depth == 0:
1701
            return stop_revid
1702
    return None
358 by James Westby
Add a script to email new failures in main.
1703
1704
1705
def load_explanations():
1706
    explanations = {}
1707
    if not os.path.exists(explanations_file):
1708
        return explanations
1709
    f = open(explanations_file)
1710
    reasons = []
1711
    explanation = ""
1712
    try:
1713
        for line in f:
1714
            if line.startswith(" "):
1715
                explanation += line
1716
            else:
1717
                if explanation != "":
1718
                    for reason in reasons:
1719
                        explanations[reason] = explanation
1720
                    explanation = ""
1721
                    reasons = []
1722
                reasons.append(line[:-1])
1723
        if explanation != "":
1724
            for reason in reasons:
1725
                explanations[reason] = explanation
1726
        return explanations
1727
    finally:
1728
        f.close()
360 by James Westby
Produce a main.html and speed up email_failures.
1729
1730
1731
def update_list(release, component, source_url):
1732
    target_dir = os.path.join(lists_dir, "dists", release, component, "source")
1733
    if not os.path.exists(target_dir):
1734
        os.makedirs(target_dir)
1735
    target_path = os.path.join(target_dir, "Sources.gz")
1736
    target_lcfile = target_path + ".lc"
1737
    last_check = None
1738
    if os.path.exists(target_lcfile):
1739
        f = open(target_lcfile)
1740
        try:
1741
            line = f.read().strip()
1742
            try:
1743
                last_check = int(line)
1744
            except ValueError:
1745
                last_check = None
1746
        finally:
1747
            f.close()
1748
    if last_check is not None:
1749
        now = time.time()
1750
        if now < last_check + (60*60):
1751
            return
1752
    target_tsfile = target_path + ".ts"
1753
    ts = None
1754
    if os.path.exists(target_tsfile):
1755
        f = open(target_tsfile)
1756
        try:
1757
            ts = f.read().strip()
1758
        finally:
1759
            f.close()
1760
    req = urllib2.Request(source_url)
1761
    if ts is not None:
1762
        req.add_header('If-Modified-Since', ts)
1763
    opener = urllib2.build_opener()
1764
    try:
1765
        resp = opener.open(req)
1766
    except urllib2.HTTPError, e:
1767
        if e.code != 304:
1768
            raise
1769
    else:
1770
        f = open(target_path, "wb")
1771
        try:
1772
            shutil.copyfileobj(resp, f)
1773
        finally:
1774
            f.close()
1775
        ts = resp.headers.get('Last-Modified')
1776
        f = open(target_tsfile, "wb")
1777
        try:
1778
            f.write("%s\n" % ts)
1779
        finally:
1780
            f.close()
1781
        f = open(target_lcfile, "wb")
1782
        try:
1783
            f.write("%d\n" % time.time())
1784
        finally:
1785
            f.close()