Merge lp:~jcsackett/charmworld/better-latency-round-2 into lp:charmworld

Proposed by j.c.sackett
Status: Merged
Approved by: j.c.sackett
Approved revision: 461
Merged at revision: 459
Proposed branch: lp:~jcsackett/charmworld/better-latency-round-2
Merge into: lp:charmworld
Diff against target: 374 lines (+182/-76)
3 files modified
.bzrignore (+1/-0)
charmworld/jobs/review.py (+74/-30)
charmworld/jobs/tests/test_review.py (+107/-46)
To merge this branch: bzr merge lp:~jcsackett/charmworld/better-latency-round-2
Reviewer Review Type Date Requested Status
Juju Gui Bot continuous-integration Approve
Aaron Bentley (community) Approve
Review via email: mp+195091@code.launchpad.net

Commit message

Undoes the rollback for the better latency work, with a fix for the expiring credentials.

Description of the change

This undoes the rollback in r 443 of the better latency work originally done in
r 422.

The majority of the changes were explained and approved in an earlier MP[1].

charmworld/jobs/review.py
-------------------------
The reinstated call to login_with is updated to use consumer_name as a
parameter; this ensures that LP sees the same, already authorized consumer when
the request occurs in dev, staging, or production.

As before, the credentials file will have to be set on the staging and
production instances by setting the lp_credentials parameter on the charm, via
juju set. IS docs on wiki.canonical.com and the deploy script for staging will
both be updated with this information after this branch is QAed.

[1]: https://code.launchpad.net/~jcsackett/charmworld/better-latency-refactor/+merge/191615

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

Looks good to me.

review: Approve
Revision history for this message
Juju Gui Bot (juju-gui-bot) wrote :

FAILED: Autolanding.
More details in the following jenkins job:
http://162.213.35.27:8080/job/charmworld-autoland-lxc/81/
Executed test runs:

review: Needs Fixing (continuous-integration)
Revision history for this message
j.c.sackett (jcsackett) wrote :

Looks like it just failed on the npm step, possible network error? Trying again.

Revision history for this message
Juju Gui Bot (juju-gui-bot) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2013-10-17 15:58:13 +0000
+++ .bzrignore 2013-11-13 16:15:48 +0000
@@ -29,3 +29,4 @@
29supervisord.log29supervisord.log
30supervisord.pid30supervisord.pid
31_run31_run
32charmbot_credentials.txt
3233
=== modified file 'charmworld/jobs/review.py'
--- charmworld/jobs/review.py 2013-11-05 18:40:38 +0000
+++ charmworld/jobs/review.py 2013-11-13 16:15:48 +0000
@@ -38,10 +38,21 @@
38 # older than the window that is still open. Right now this is probably the38 # older than the window that is still open. Right now this is probably the
39 # more performant way to go about it, but that's going to change as the39 # more performant way to go about it, but that's going to change as the
40 # data outside of our window gets larger.40 # data outside of our window gets larger.
41 try:
42 lp_credentials = get_ini()['lp_credentials_file']
43 except KeyError:
44 lp_credentials = None
41 with Timer() as timer:45 with Timer() as timer:
42 lp = Launchpad.login_anonymously('charm-tools',46 if lp_credentials:
43 'production',47 lp = Launchpad.login_with(application_name='charm-tools',
44 version='devel')48 service_root='production',
49 version='devel',
50 consumer_name='charmworld',
51 credentials_file=lp_credentials)
52 else:
53 lp = Launchpad.login_anonymously('charm-tools',
54 'production',
55 version='devel')
4556
46 # Entity roots for items of interests57 # Entity roots for items of interests
47 charm = lp.distributions['charms']58 charm = lp.distributions['charms']
@@ -50,13 +61,13 @@
5061
51 log.debug("lp login time %0.2f", timer.duration())62 log.debug("lp login time %0.2f", timer.duration())
5263
64 branch_filter = "Show only Bugs with linked Branches"
53 with Timer() as timer:65 with Timer() as timer:
54 bugs = charm.searchTasks(66 bugs = charm.searchTasks(
55 tags=['new-formula', 'new-charm'],67 tags=['new-formula', 'new-charm'],
56 tags_combinator="Any")68 tags_combinator="Any",
57 # Best practice dictates that charmers be subscribed to all bugs69 linked_branches=branch_filter)
58 # and merge proposals for official source package branches.70 charmers_bugs = charmers.searchTasks(linked_branches=branch_filter)
59 charmers_bugs = charmers.searchTasks()
60 proposals = charmers.getRequestedReviews()71 proposals = charmers.getRequestedReviews()
61 contributor_proposals = charm_contributors.getRequestedReviews()72 contributor_proposals = charm_contributors.getRequestedReviews()
6273
@@ -69,27 +80,67 @@
69 itertools.chain(proposals, contributor_proposals, doc_proposals))80 itertools.chain(proposals, contributor_proposals, doc_proposals))
7081
7182
83def calculate_proposal_latency(proposal, now):
84 if proposal.date_reviewed:
85 latency = (
86 proposal.date_reviewed - proposal.date_review_requested)
87 else:
88 # `now` has been calculated in UTC, and LP returns UTC TZ
89 # datetimes. We can safely remove the TZ, letting us get the
90 latency = (
91 now - proposal.date_review_requested.replace(tzinfo=None))
92 return latency.total_seconds()
93
94
95def calculate_task_latency(task, now):
96 link_entries = [e for e in task.bug.activity.entries if
97 e['whatchanged'] == 'branch linked']
98 if len(link_entries) == 1:
99 start_date = link_entries[0]['datechanged']
100 start_date = datetime.datetime.strptime(
101 start_date.split('.')[0], '%Y-%m-%dT%H:%M:%S')
102 elif len(link_entries) > 1:
103 #Filter down to links associated with the currently linked branches
104 linked_branches = task.bug.linked_branches
105 branch_names = [link.branch.display_name for link in linked_branches]
106 current_entries = [e for e in link_entries if
107 e['newvalue'] in branch_names]
108 if current_entries != []:
109 start_date = current_entries[0]['datechanged']
110 start_date = datetime.datetime.strptime(
111 start_date.split('.')[0], '%Y-%m-%dT%H:%M:%S')
112 else:
113 # At some point, the name of the branch changed, so we can't find
114 # the event when it was added. This happens rarely, so we just
115 # abandon stats for this task.
116 return
117 elif link_entries == []:
118 # We have no entries, probably b/c the lp step failed to login with
119 # authentication.
120 return
121
122 if task.date_fix_committed:
123 end_date = task.date_fix_committed.replace(tzinfo=None)
124 else:
125 end_date = now
126 latency = end_date - start_date
127 return latency.total_seconds()
128
129
72def update_review_queue(db, tasks, proposals, log):130def update_review_queue(db, tasks, proposals, log):
73 '''Updates the review queue information.'''131 '''Updates the review queue information.'''
132 now = datetime.datetime.utcnow()
133 window = now - datetime.timedelta(days=180) # 6 month window for latency
74 bug_statuses = [134 bug_statuses = [
75 'New', 'Confirmed', 'Triaged', 'In Progress', 'Fix Committed']135 'New', 'Confirmed', 'Triaged', 'In Progress', 'Fix Committed']
76 proposal_statuses = ['Needs review', 'Needs fixing', 'Needs information']
77 now = datetime.datetime.utcnow()
78 window = now - datetime.timedelta(days=180) # 6 month window for latency
79
80 r_queue = []136 r_queue = []
81 latencies = []137 latencies = []
82 for task in tasks:138 for task in tasks:
83 if (task.date_created.replace(tzinfo=None) >= window or139 if (task.date_created.replace(tzinfo=None) >= window or
84 task.status in bug_statuses):140 task.status in bug_statuses):
85 if task.date_confirmed:141 latency = calculate_task_latency(task, now)
86 latency = task.date_confirmed - task.date_created142 if latency:
87 else:143 latencies.append(latency)
88 # Now has been calculated in UTC, and LP returns UTC TZ
89 # datetimes. We can safely remove the TZ, letting us get the
90 # timedelta.
91 latency = now - task.date_created.replace(tzinfo=None)
92 latencies.append(latency.total_seconds())
93144
94 if task.status in bug_statuses:145 if task.status in bug_statuses:
95 entry = {146 entry = {
@@ -102,19 +153,13 @@
102 }153 }
103 r_queue.append(entry)154 r_queue.append(entry)
104155
156 proposal_statuses = [
157 'Needs review', 'Needs fixing', 'Needs information']
105 for proposal in proposals:158 for proposal in proposals:
106 if (proposal.date_created.replace(tzinfo=None) >= window or159 if (proposal.date_created.replace(tzinfo=None) >= window or
107 proposal.queue_status in proposal_statuses):160 proposal.queue_status in proposal_statuses):
108 if proposal.date_reviewed:161 latency = calculate_proposal_latency(proposal, now)
109 latency = (162 latencies.append(latency)
110 proposal.date_reviewed - proposal.date_review_requested)
111 else:
112 # Now has been calculated in UTC, and LP returns UTC TZ
113 # datetimes. We can safely remove the TZ, letting us get the
114 latency = (
115 now - proposal.date_review_requested.replace(tzinfo=None))
116 latencies.append(latency.total_seconds())
117
118 if proposal.queue_status in proposal_statuses:163 if proposal.queue_status in proposal_statuses:
119 parts = proposal.target_branch_link.rsplit('/', 3)164 parts = proposal.target_branch_link.rsplit('/', 3)
120 if parts[-1] == "trunk":165 if parts[-1] == "trunk":
@@ -131,7 +176,6 @@
131176
132 entry = {177 entry = {
133 'type': 'proposal',178 'type': 'proposal',
134 'latency': latency.total_seconds(),
135 'date_modified': last_modified,179 'date_modified': last_modified,
136 'date_created': proposal.date_created,180 'date_created': proposal.date_created,
137 'summary': "Merge for %s from %s" % (target, origin),181 'summary': "Merge for %s from %s" % (target, origin),
138182
=== modified file 'charmworld/jobs/tests/test_review.py'
--- charmworld/jobs/tests/test_review.py 2013-11-05 18:40:38 +0000
+++ charmworld/jobs/tests/test_review.py 2013-11-13 16:15:48 +0000
@@ -3,7 +3,13 @@
3import datetime3import datetime
4import logging4import logging
55
6from charmworld.jobs.review import update_review_queue6import mock
7
8from charmworld.jobs.review import (
9 calculate_proposal_latency,
10 calculate_task_latency,
11 update_review_queue,
12)
7from charmworld.testing import MongoTestBase13from charmworld.testing import MongoTestBase
814
915
@@ -12,45 +18,45 @@
12TWO_DAYS = NOW - datetime.timedelta(2)18TWO_DAYS = NOW - datetime.timedelta(2)
1319
1420
15class MockBug:21def get_mock_task(confirmed=False, entries=[], linked_branches=[]):
16 date_last_updated = NOW22 if confirmed:
1723 status = "Confirmed"
1824 date_fix_committed = YESTERDAY
19class MockTask:25 else:
2026 date_fix_committed = None
21 def __init__(self, confirmed=False):27 status = "New"
22 self.date_created = TWO_DAYS28 return mock.Mock(
23 self.title = u'Bug #0 in a collection: "A bug task"'29 status=status,
24 self.web_link = "http://example.com"30 date_fix_committed=date_fix_committed,
25 self.bug = MockBug()31 date_created=TWO_DAYS,
26 if confirmed:32 title=u'Bug #0 in a collection: "A bug task"',
27 self.status = "Confirmed"33 web_link="http://example.com",
28 self.date_confirmed = YESTERDAY34 bug=mock.Mock(
29 else:35 date_last_updated=NOW,
30 self.date_confirmed = None36 linked_branches=linked_branches,
31 self.status = "New"37 activity=mock.Mock(entries=entries)
3238 ),
3339 )
34class MockCommentCollection:40
35 total_size = 041
3642def get_mock_proposal(reviewed=False):
3743 base = 'https://api.launchpad.net/devel/'
38class MockProposal:44 if reviewed:
3945 date_reviewed = YESTERDAY
40 def __init__(self, reviewed=False):46 queue_status = "Approved"
41 base = 'https://api.launchpad.net/devel/'47 else:
42 self.target_branch_link = base + '~charmers/charms/precise/foo/trunk'48 date_reviewed = None
43 self.source_branch_link = base + '~bar/charms/precise/foo/fnord'49 queue_status = "Needs review"
44 self.all_comments = MockCommentCollection()50 return mock.Mock(
45 self.date_review_requested = TWO_DAYS51 target_branch_link=base + '~charmers/charms/precise/foo/trunk',
46 self.date_created = TWO_DAYS52 source_branch_link=base + '~bar/charms/precise/foo/fnord',
47 self.web_link = 'http://example.com'53 all_comments=mock.Mock(total_size=0),
48 if reviewed:54 date_review_requested=TWO_DAYS,
49 self.date_reviewed = YESTERDAY55 date_created=TWO_DAYS,
50 self.queue_status = "Approved"56 web_link='http://example.com',
51 else:57 date_reviewed=date_reviewed,
52 self.date_reviewed = None58 queue_status=queue_status,
53 self.queue_status = "Needs review"59 )
5460
5561
56class ReviewTest(MongoTestBase):62class ReviewTest(MongoTestBase):
@@ -63,7 +69,7 @@
6369
64 def test_review_handles_bugs(self):70 def test_review_handles_bugs(self):
65 log = logging.getLogger('foo')71 log = logging.getLogger('foo')
66 update_review_queue(self.db, [MockTask()], [], log)72 update_review_queue(self.db, [get_mock_task()], [], log)
67 self.assertEqual(1, self.db.review_queue.count())73 self.assertEqual(1, self.db.review_queue.count())
68 item = self.db.review_queue.find_one()74 item = self.db.review_queue.find_one()
69 self.assertEqual(NOW.date(), item['date_modified'].date())75 self.assertEqual(NOW.date(), item['date_modified'].date())
@@ -74,7 +80,7 @@
7480
75 def test_review_handles_proposals(self):81 def test_review_handles_proposals(self):
76 log = logging.getLogger('foo')82 log = logging.getLogger('foo')
77 update_review_queue(self.db, [], [MockProposal()], log)83 update_review_queue(self.db, [], [get_mock_proposal()], log)
78 self.assertEqual(1, self.db.review_queue.count())84 self.assertEqual(1, self.db.review_queue.count())
79 item = self.db.review_queue.find_one()85 item = self.db.review_queue.find_one()
80 self.assertEqual(TWO_DAYS.date(), item['date_modified'].date())86 self.assertEqual(TWO_DAYS.date(), item['date_modified'].date())
@@ -84,10 +90,65 @@
84 self.assertEqual('http://example.com', item['item'])90 self.assertEqual('http://example.com', item['item'])
85 self.assertEqual('Needs review', item['status'])91 self.assertEqual('Needs review', item['status'])
8692
87 def test_review_calculates_latency(self):93 def test_calculate_task_latency_with_two_valid_links(self):
94 linked_branches = [
95 mock.Mock(branch=mock.Mock(display_name='lp:~foo/bar/bjorn')),
96 mock.Mock(branch=mock.Mock(display_name='lp:~foo/bar/baz'))
97 ]
98 two_days = datetime.datetime.strftime(
99 TWO_DAYS, '%Y-%m-%dT%H:%M:%S')
100 yesterday = datetime.datetime.strftime(
101 YESTERDAY, '%Y-%m-%dT%H:%M:%S')
102 entries = [
103 {'whatchanged': 'branch linked',
104 'newvalue': 'lp:~foo/bar/baz',
105 'datechanged': two_days},
106 {'whatchanged': 'branch linked',
107 'newvalue': 'lp:~foo/bar/bjorn',
108 'datechanged': yesterday}]
109 latency = calculate_task_latency(
110 get_mock_task(
111 True, entries=entries, linked_branches=linked_branches), NOW)
112 self.assertEqual(60 * 60 * 24, int(latency))
113
114 def test_calculate_task_latency_bug_with_one_valid_link(self):
115 linked_branches = [
116 mock.Mock(branch=mock.Mock(display_name='lp:~foo/bar/bjorn'))
117 ]
118 two_days = datetime.datetime.strftime(
119 TWO_DAYS, '%Y-%m-%dT%H:%M:%S')
120 yesterday = datetime.datetime.strftime(
121 YESTERDAY, '%Y-%m-%dT%H:%M:%S')
122 entries = [
123 {'whatchanged': 'branch linked',
124 'newvalue': 'lp:~foo/bar/baz',
125 'datechanged': yesterday},
126 {'whatchanged': 'branch linked',
127 'newvalue': 'lp:~foo/bar/bjorn',
128 'datechanged': two_days}]
129 latency = calculate_task_latency(
130 get_mock_task(
131 True, entries=entries, linked_branches=linked_branches), NOW)
132 self.assertEqual(60 * 60 * 24, int(latency))
133
134 def test_calculate_task_latency_bug_with_one_link(self):
135 date_changed = datetime.datetime.strftime(
136 TWO_DAYS, '%Y-%m-%dT%H:%M:%S')
137 entries = [{'whatchanged': 'branch linked',
138 'newvalue': 'lp:~foo/bar/baz',
139 'datechanged': date_changed}]
140 latency = calculate_task_latency(
141 get_mock_task(True, entries=entries), NOW)
142 self.assertEqual(60 * 60 * 24, int(latency))
143
144 def test_calculate_proposal_latency(self):
145 latency = calculate_proposal_latency(get_mock_proposal(True), NOW)
146 self.assertEqual(60 * 60 * 24, int(latency))
147
148 def test_review_job_saves_latency(self):
88 log = logging.getLogger('foo')149 log = logging.getLogger('foo')
89 self.assertEqual(0, self.db.review_queue_latency.count())150 self.assertEqual(0, self.db.review_queue_latency.count())
90 update_review_queue(self.db, [], [MockProposal(True)], log)151 update_review_queue(self.db, [], [get_mock_proposal(True)], log)
91 self.assertEqual(1, self.db.review_queue_latency.count())152 self.assertEqual(1, self.db.review_queue_latency.count())
92 item = self.db.review_queue_latency.find_one()153 item = self.db.review_queue_latency.find_one()
93 self.assertEqual(NOW.date(), item['date'].date())154 self.assertEqual(NOW.date(), item['date'].date())
@@ -96,14 +157,14 @@
96 self.assertEqual(1, datetime.timedelta(seconds=item['min']).days)157 self.assertEqual(1, datetime.timedelta(seconds=item['min']).days)
97158
98 def test_review_discards_closed_bugs(self):159 def test_review_discards_closed_bugs(self):
99 tasks = [MockTask(), MockTask()]160 tasks = [get_mock_task(), get_mock_task()]
100 tasks[0].status = 'Fix released'161 tasks[0].status = 'Fix released'
101 log = logging.getLogger('foo')162 log = logging.getLogger('foo')
102 update_review_queue(self.db, tasks, [], log)163 update_review_queue(self.db, tasks, [], log)
103 self.assertEqual(1, self.db.review_queue.count())164 self.assertEqual(1, self.db.review_queue.count())
104165
105 def test_review_discards_approved_proposals(self):166 def test_review_discards_approved_proposals(self):
106 proposals = [MockProposal(), MockProposal()]167 proposals = [get_mock_proposal(), get_mock_proposal()]
107 proposals[0].queue_status = 'Approved'168 proposals[0].queue_status = 'Approved'
108 log = logging.getLogger('foo')169 log = logging.getLogger('foo')
109 update_review_queue(self.db, [], proposals, log)170 update_review_queue(self.db, [], proposals, log)

Subscribers

People subscribed via source and target branches

to all changes: