Merge lp:~achiang/hydrazine/batch-operations into lp:hydrazine

Proposed by Alex Chiang
Status: Merged
Merged at revision: 93
Proposed branch: lp:~achiang/hydrazine/batch-operations
Merge into: lp:hydrazine
Diff against target: 243 lines (+120/-23)
2 files modified
README (+23/-1)
bugclient (+97/-22)
To merge this branch: bzr merge lp:~achiang/hydrazine/batch-operations
Reviewer Review Type Date Requested Status
Martin Pool Needs Fixing
Martin Packman (community) Needs Information
Review via email: mp+78702@code.launchpad.net

Description of the change

batchify existing commands

Lots of user-visible documentation written, and even more developer documentation in the individual commits.

Please take a look and let me know what you think.

Depends on:
https://code.launchpad.net/~achiang/hydrazine/encapsulate-validation/+merge/78697

Thanks.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

This looks really interesting. So the main aim of the change is to generalise the way of showing new bugs into a new 'filter' command.

What kind of performance do you get against staging with the batch command? I don't know the launchpad api all that well and am a little concerned that just delegating out to the other do_ methods for each bug is not the best way of changing lots of bugs. Currently having an lp_save in each command isn't do bad because it's generally limited by your typing speed, but if changing importance, validity and tags on 100 bugs means 300 saves, I don't know if that's will get us yelled at.

Merge proposal nit, when you have a branch that depends on a change you've proposed separately, remember to set the 'prerequisite branch' option so those changes aren't show in the diff.

review: Needs Information
Revision history for this message
Martin Pool (mbp) wrote :

This would be a great feature to have, thanks achiang!

[fix] Please mention this in the readme.

[aside] It might be nice to actually split out the concept of 'active filter' into an object so that we can refine it later, but that can be changed later on.

> What kind of performance do you get against staging with the batch command?
> Currently having an lp_save in each command isn't do bad because it's generally limited by your typing speed, but if changing importance, validity and tags on 100 bugs means 300 saves, I don't know if that's will get us yelled at.

I think the thing that _will_ get us yelled at is if a slip of the finger in a batch command ends up with thousands of bugs accidentally being closed. So that's probably best done through [not having any bugs ;-)] and making it clear what's going to be done before anything happens, or at least before too much of it happens to be irretrievable. For instance we could print 'setting 213 bugs to status closed' as we start.

With performance: generally speaking having a new feature that's slow is better than not having the feature at all, as long as it's not done in a way that will be inordinately hard to improve later. And, in this case, we're automating something previously done manually so even slow operation can only help.

Each lp_save will do a blocking round trip, so if you update 100 bugs it will take some time (depending on rtt). Launchpad doesn't have a 'update multiple bugs' rpc, so to do better we either need to add new apis or (perhaps more likely) overlap multiple requests. The latter may be hard from lplib, and we might need eventually to go to twisted or golang or whatever, but either of them is out of scope.

So in short: good question, but it shouldn't block merging this imo.

[aside] This is getting to the point where it needs some testing, preferably against a mock Launchpad.

[aside] Perhaps hydrazine should print a 'what's new' when it starts.

[fix] 'filter tags' doesn't seem to be implemented?

review: Needs Fixing
100. By Alex Chiang

bugclient: merge with trunk to get NamedEnum() class

Get latest NamedEnum() class from trunk, and convert our old filter
enum to use it properly.

101. By Alex Chiang

README: updated to reflect new batch and filter commands

Update the readme, now that select_new is gone.

Add some helpful examples on how to use filter and batch.

Revision history for this message
Alex Chiang (achiang) wrote :

Hello gz,

> This looks really interesting. So the main aim of the change is to generalise the way of showing new bugs into a new 'filter' command.

Actually, processing new bugs was quite a minor consideration, it just happens that the new batch and filter commands can be used to do so (hopefully a good sign for the general soundness of the design!).

The main impetus is that I'm involved with projects where we target 30--60 bugs to a milestone, and as developers fix them, they move status to Fix Committed. After the project is QA'ed and released, the milestone is closed, but now we have to change the status on all those bugs to Fix Released.

I have future plans to extend the filters even further, so we can do things like, "select bugs marked fix committed last week", but that will obviously wait for a future MP.

Like poolie, I would prefer to shelve the performance discussion for a later MP as well.

I've updated README as requested by poolie.

Would love it if folks could re-review, thanks!

Revision history for this message
Martin Pool (mbp) wrote :

thanks for the doc updates.

72 +Caution, without the 'filter' command, you will operate on ALL
73 +active bugs in your pillar, including bugs already Fix Committed.

[fix] That sounds kind of risky. Would it be at all practical to have it just fail if there is no filter? I think it would, just by after the 'for f' block

   else:
        print 'No filter set?'
        return

aside from that it looks good - thanks!

If you just fix that thing, please go ahead and land it.

review: Needs Fixing
102. By Alex Chiang

bugclient: require a filter before doing batch commands

This requirement makes life a little safer. Without it, we had the
potential to do a batch operation on ALL active bugs in the pillar,
which includes bugs with status 'Fix Committed'.

If we require a filter, that can mitigate this little pitfall.

Revision history for this message
Alex Chiang (achiang) wrote :

Fixed, and now landing, per poolie's approval.

Thanks all!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README'
2--- README 2010-09-02 01:16:39 +0000
3+++ README 2011-10-12 03:20:22 +0000
4@@ -20,13 +20,35 @@
5
6 ./bugclient
7 pillar bzr
8- select_new
9+ filter status new
10+ batch
11 bug 1234
12 triage confirmed high +dirstate
13 next
14
15 ``triage`` can set the importance, status, and tags in one go.
16
17+In the example above, we use the plain ``batch`` command after selecting all
18+bugs with status New, and iterate over them one at a time, with ``next``.
19+
20+A more interesting example of how to use the batch command might be to
21+change the status of many bugs after a project has released a milestone::
22+
23+ ./bugclient
24+ pillar hydrazine
25+ filter milestone 0.2
26+ filter status Fix Committed # note, you may apply multiple filters
27+ batch status Fix Released
28+
29+Or perhaps you would like to tag multiple bugs at once::
30+
31+ ./bugclient
32+ pillar hydrazine
33+ filter importance wishlist
34+ batch tag +bluesky
35+
36+See the help for ``filter`` and ``batch`` for more details.
37+
38 feed-pqm
39 ========
40
41
42=== modified file 'bugclient'
43--- bugclient 2011-10-11 02:39:41 +0000
44+++ bugclient 2011-10-12 03:20:22 +0000
45@@ -38,6 +38,7 @@
46 'Incomplete', 'Invalid', 'New'])
47 importance_enum = NamedEnum(['Critical', 'High', 'Medium', 'Low',
48 'Wishlist', 'Undecided'])
49+filter_enum = NamedEnum(['milestone', 'status', 'tags', 'clear'])
50
51 class HydrazineCmd(cmd.Cmd):
52
53@@ -47,10 +48,61 @@
54 self.current_bug_number = None
55 self.pillar = None
56 self.task_list = None
57+ self.filters = {}
58
59 def _connect(self):
60 self.session = hydrazine.create_session()
61
62+ def do_batch(self, line):
63+ """Perform batch operation on current pillar's bugs.
64+Use the 'filter' command to limit the batch command's scope of bugs.
65+
66+example:
67+ filter status new
68+ batch # load New bugs; iterate manually with next
69+ batch show # show information on all New bugs
70+ batch status confirmed # change all New bugs to Confirmed
71+
72+Caution, there is very little error checking. You are about to commit
73+a lot of changes to a lot of bugs. Double-check your command!
74+ """
75+ kwargs = {}
76+ if not self.filters:
77+ print 'No filter set?'
78+ return
79+ for f in self.filters:
80+ if f == "milestone":
81+ kwargs[f] = "%s/+milestone/%s" % (self.pillar, self.filters[f])
82+ else:
83+ kwargs[f] = self.filters[f]
84+ # XXX: is this the best sort order?
85+ kwargs['order_by'] = '-datecreated'
86+ self.task_list = self.pillar.searchTasks(**kwargs)
87+
88+ try:
89+ first_bug_task = self.task_list[0]
90+ self.search_index = 0
91+ self._select_bug(first_bug_task.bug, "quiet")
92+ except IndexError:
93+ print "No bugtasks found"
94+ return
95+
96+ args = line.split()
97+ if not args:
98+ print "Task list loaded"
99+ self._show_bug(self.bug)
100+ return
101+
102+ # XXX: blind pass-through for now; need to prevent commands that
103+ # make no sense in batch mode, such as 'official_tags', etc.
104+ for tasks in self.task_list:
105+ getattr(self, "do_" + args[0])(' '.join(args[1:]))
106+ self.do_next("quiet")
107+
108+ # go back to beginning of task_list to do more batch commands
109+ self.search_index = 0
110+ self._select_bug(self.task_list[0].bug, "quiet")
111+
112 def do_bug(self, bug_number):
113 """Open bug by number"""
114 try:
115@@ -106,6 +158,36 @@
116 def do_EOF(self, what):
117 return True
118
119+ def do_filter(self, line):
120+ """Add a filter for batch operations
121+
122+examples:
123+ filter milestone M1 # milestone named 'M1'
124+ filter status new # all New bugs
125+ filter tags a b c # tags 'a', 'b', and 'c'
126+ filter clear # remove all filters
127+ """
128+ args = line.split()
129+ if not filter_enum.get(args[0]):
130+ print 'Invalid filter. Valid filters are:'
131+ _show_columnated(filter_enum.get_all())
132+ return
133+
134+ if args[0] == "clear":
135+ self.filters = {}
136+ return
137+ elif args[0] == "status":
138+ if status_enum.get(' '.join(args[1:])) == None:
139+ print 'invalid status, valid values are:'
140+ _show_columnated(status_enum.get_all())
141+ return
142+ else:
143+ # Normalize status, and toss old junk
144+ args[1] = status_enum.get(' '.join(args[1:]))
145+ del args[2:]
146+
147+ self.filters[args[0]] = ' '.join(args[1:])
148+
149 def do_importance(self, line):
150 """Set importance"""
151 task = self._needs_single_task()
152@@ -116,10 +198,10 @@
153 print 'invalid importance, valid values are:'
154 _show_columnated(importance_enum.get_all())
155 return
156- print 'changing importance %s => %s' % (task.importance, new_importance)
157+ print '#%d changing importance %s => %s' % (self.bug.id, task.importance, new_importance)
158 task.importance = new_importance
159- print '**** before save'
160 if opts.debug:
161+ print '**** before save'
162 print task._wadl_resource._definition.representation
163 try:
164 task.lp_save()
165@@ -128,10 +210,10 @@
166 if opts.debug:
167 print task._wadl_resource._definition.representation
168
169- def do_next(self, ignored):
170+ def do_next(self, show):
171 """Go to the next bug in the list"""
172 if self.task_list is None:
173- print 'no list loaded; use select_new etc'
174+ print 'no list loaded; use a filter and batch to load a list'
175 return
176 self.search_index += 1
177 try:
178@@ -139,7 +221,9 @@
179 except IndexError:
180 print "End of bug list"
181 return
182- self._select_bug(bug_task.bug)
183+ if show == "":
184+ show = "show"
185+ self._select_bug(bug_task.bug, show)
186
187 def do_official_tags(self, ignored):
188 """Show the official tags for the current pillar."""
189@@ -181,19 +265,6 @@
190 task.target = new_target
191 task.lp_save()
192
193- def do_select_new(self, ignored):
194- """Select the list of new bugs in the current pillar"""
195- if self._needs_pillar(): return
196- self.task_list = self.pillar.searchTasks(status="New",
197- order_by=['-datecreated'])
198- self.task_list_index = 0
199- try:
200- first_bug_task = self.task_list[0]
201- self.search_index = 0
202- except IndexError:
203- print "No bugtasks found"
204- self._select_bug(first_bug_task.bug)
205-
206 def do_show(self, ignored):
207 """Show the header of the current bug"""
208 if self._needs_bug():
209@@ -229,7 +300,7 @@
210 print 'invalid status, valid values are:'
211 _show_columnated(status_enum.get_all())
212 return
213- print 'changing status %s => %s' % (task.status, new_status)
214+ print '#%d changing status %s => %s' % (self.bug.id, task.status, new_status)
215 task.status = new_status
216 task.lp_save()
217
218@@ -343,7 +414,10 @@
219
220 @property
221 def prompt(self):
222- p = 'hydrazine(%s) ' % (self.short_service_root,)
223+ p = ""
224+ if self.filters:
225+ p = 'Current filters: %s\n' % self.filters
226+ p += 'hydrazine(%s) ' % (self.short_service_root,)
227 if self.current_bug_number is not None:
228 p += '#%d ' % (self.current_bug_number,)
229 if self.pillar is not None:
230@@ -355,10 +429,11 @@
231 p = p[:-1]
232 return p + '> '
233
234- def _select_bug(self, the_bug):
235+ def _select_bug(self, the_bug, show="show"):
236 self.bug = the_bug
237 self.current_bug_number = the_bug.id
238- self._show_bug(self.bug)
239+ if show == "show":
240+ self._show_bug(self.bug)
241
242 def _find_pillar(self, pillar_name):
243 pillar_collection = self.session.pillars.search(text=pillar_name)

Subscribers

People subscribed via source and target branches

to all changes: