Merge lp:~jtv/launchpad/bug-452107 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~jtv/launchpad/bug-452107
Merge into: lp:launchpad
Diff against target: 100 lines
4 files modified
lib/lp/code/interfaces/branchjob.py (+5/-3)
lib/lp/code/model/branchjob.py (+8/-5)
lib/lp/translations/doc/translations-export-to-branch.txt (+17/-0)
lib/lp/translations/scripts/translations_to_branch.py (+5/-1)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-452107
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+13615@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (4.2 KiB)

= Bug 452107 =

We have a script that exports the latest translations for a product
series to a bzr branch. Another script allows users to import their
translations from a bzr branch. There's nothing stopping a user from
setting up both on the same product series and the same branch, creating
a full-duplex exchange of translations between the branch and Rosetta.

To avoid all sorts of race conditions, the export-to-bzr script checks
that there are no pending BranchJobs tasked with putting changes coming
in from the branch onto the translations import queue. If there are
pending jobs of this kind, it skips the export.

Alas! BranchJobs have no garbage collection or status monitoring of any
kind. So if a job dies in a way that circumvents its status change to
"failed" (e.g. because of a server reset or a script kill), the zombie
job sticks around in uncompleted state and haunts us forever.

This branch copes with that by assuming that a RosettaUploadJob (the
type of BranchJob in question) will always be completed, one way or the
other, within one day. Any jobs older than that are ignored.

I implemented this by adding a "since" parameter to the method that
finds unfinished jobs. It still defaults to an unlimited search because
the method may conceivably be used in the future to find zombie jobs
instead of the pending ones that aren't zombies.

Test: {{{./bin/test -vv -t translations.*to.branch}}}

Staging Q/A is a pain in the arse:

 * Pick a project that you have edit rights for. Ensure that it's set
   up to use Launchpad Translations.

 * Register a branch for the project on staging. Populate it by pushing
   a real branch to it. Set the branch as the development branch for a
   translatable release series of your product.

 * Set up translations export for the product series to your branch.

 * Set up translations import from bzr for the product series (of both
   template and translations).

 * Add a .pot file to your branch, commit, and push.

 * Wait for the rosetta-branches script to run on staging. (Make sure
   this has been set up; mthaddon started on it 2009-10-19 but I haven't
   checked up at the time of writing). This can take up to an hour.

 * Verify that your template appears on the project's import queue.
   Wait for it to be imported.

 * Translate something in the UI.

 * Have cronscripts/translations-export-to-branch.py run on the staging
   codehosting server. Never mind the horrible failures for the other
   branches; they're not available on this server.

 * Check that this committed your UI translation to your branch.

So far we've verified that I haven't broken exports, but not the actual
fix yet. Have a LOSA execute this SQL against the staging db to
simulate zombification of the job that just imported your template:

    UPDATE Job
    SET status = 1
    FROM Product, ProductSeries, BranchJob
    WHERE
        Job.id = BranchJob.job AND
        ProductSeries.branch = BranchJob.branch AND
        Product.id = ProductSeries.product AND
        Product.name = '<your-product-name>' AND
        ProductSeries.name = '<your-productseries-name>' AND
        BranchJob.job_type = 3;

This should update 1 record....

Read more...

Revision history for this message
Brad Crittenden (bac) wrote :

This fix looks good Jeroen. Are there plans to do some garbage collection on those zombies?

review: Approve (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> This fix looks good Jeroen. Are there plans to do some garbage collection on
> those zombies?

I believe there are; but that's purely a Code problem and we couldn't wait for it.

Separately from that, I believe Henning has requested that "running" jobs be cleared out during rollouts, since at that point we know that no jobs are really still running.

Thanks for the review!

Revision history for this message
Jonathan Lange (jml) wrote :

On Wed, Oct 21, 2009 at 6:10 AM, Jeroen T. Vermeulen <email address hidden> wrote:
>> This fix looks good Jeroen.  Are there plans to do some garbage collection on
>> those zombies?
>
> I believe there are; but that's purely a Code problem and we couldn't wait for it.
>

It'd still be nice to mention the bug number in your doctest, so you
know to fix it when the code bug is fixed.

jml

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/interfaces/branchjob.py'
2--- lib/lp/code/interfaces/branchjob.py 2009-08-13 07:10:18 +0000
3+++ lib/lp/code/interfaces/branchjob.py 2009-10-20 06:50:31 +0000
4@@ -158,11 +158,13 @@
5 """Iterate through ready IRosettaUploadJobs."""
6
7
8- def findUnfinishedJobs(branch):
9+ def findUnfinishedJobs(branch, since=None):
10 """Find any `IRosettaUploadJob`s for `branch` that haven't run yet.
11
12- Returns ready jobs, but also ones in any other state except
13- "complete" or "failed."
14+ :param branch: Branch to find unfinished jobs for.
15+ :param since: Optional cutoff date: ignore jobs older than this.
16+ :return: Any jobs for `branch` (and newer than `since`, if
17+ given) whose status is neither "complete" nor "failed."
18 """
19
20
21
22=== modified file 'lib/lp/code/model/branchjob.py'
23--- lib/lp/code/model/branchjob.py 2009-10-06 20:13:13 +0000
24+++ lib/lp/code/model/branchjob.py 2009-10-20 06:50:31 +0000
25@@ -529,8 +529,8 @@
26 """
27 store = Store.of(self.branch)
28 conditions = [
29- BranchMergeProposal.target_branch==self.branch.id,
30- BranchMergeProposal.source_branch==Branch.id,
31+ BranchMergeProposal.target_branch == self.branch.id,
32+ BranchMergeProposal.source_branch == Branch.id,
33 Branch.last_scanned_id.is_in(revision_ids)]
34 if not include_superseded:
35 conditions.append(
36@@ -843,15 +843,18 @@
37 return (RosettaUploadJob(job) for job in jobs)
38
39 @staticmethod
40- def findUnfinishedJobs(branch):
41+ def findUnfinishedJobs(branch, since=None):
42 """See `IRosettaUploadJobSource`."""
43 store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
44- jobs = store.using(BranchJob, Job).find((BranchJob), And(
45+ match = And(
46 Job.id == BranchJob.jobID,
47 BranchJob.branch == branch,
48 BranchJob.job_type == BranchJobType.ROSETTA_UPLOAD,
49 Job._status != JobStatus.COMPLETED,
50- Job._status != JobStatus.FAILED))
51+ Job._status != JobStatus.FAILED)
52+ if since is not None:
53+ match = And(match, Job.date_created > since)
54+ jobs = store.using(BranchJob, Job).find((BranchJob), match)
55 return jobs
56
57
58
59=== modified file 'lib/lp/translations/doc/translations-export-to-branch.txt'
60--- lib/lp/translations/doc/translations-export-to-branch.txt 2009-09-24 08:28:54 +0000
61+++ lib/lp/translations/doc/translations-export-to-branch.txt 2009-10-20 06:50:31 +0000
62@@ -257,3 +257,20 @@
63 has pending translations changes. Not committing.
64 INFO Processed 1 item(s); 1 failure(s).
65
66+There is one problem with detecting this race condition. Jobs are never
67+cleaned up. So if the job failed for whatever reason, an unfinished job
68+will stick around forever.
69+
70+To avoid blocking on such a job forever, the check ignores jobs that are
71+old enough that they must have completed one way or another.
72+
73+ >>> job.date_created -= timedelta(days=7)
74+ >>> transaction.commit()
75+ >>> script.main()
76+ INFO Exporting to translations branches.
77+ INFO Exporting Gazblachko trunk series.
78+ DEBUG ...
79+ INFO Writing file 'po/main/nl.po':
80+ INFO ...
81+ INFO Unlock.
82+ INFO Processed 1 item(s); 0 failure(s).
83
84=== modified file 'lib/lp/translations/scripts/translations_to_branch.py'
85--- lib/lp/translations/scripts/translations_to_branch.py 2009-10-02 09:48:30 +0000
86+++ lib/lp/translations/scripts/translations_to_branch.py 2009-10-20 06:50:31 +0000
87@@ -63,8 +63,12 @@
88 "Translations export for %s was just disabled." % (
89 source.title))
90
91+ branch = source.translations_branch
92 jobsource = getUtility(IRosettaUploadJobSource)
93- if jobsource.findUnfinishedJobs(source.translations_branch).any():
94+ unfinished_jobs = jobsource.findUnfinishedJobs(
95+ branch, since=datetime.now(UTC) - timedelta(days=1))
96+
97+ if unfinished_jobs.any():
98 raise ConcurrentUpdateError(
99 "Translations branch for %s has pending translations "
100 "changes. Not committing." % source.title)