Merge lp:~facundo/ubuntuone-client/sys-quota-exceeded-back into lp:ubuntuone-client

Proposed by Facundo Batista
Status: Merged
Approved by: Facundo Batista
Approved revision: 906
Merged at revision: 904
Proposed branch: lp:~facundo/ubuntuone-client/sys-quota-exceeded-back
Merge into: lp:ubuntuone-client
Diff against target: 240 lines (+72/-22)
2 files modified
tests/syncdaemon/test_action_queue.py (+51/-17)
ubuntuone/syncdaemon/action_queue.py (+21/-5)
To merge this branch: bzr merge lp:~facundo/ubuntuone-client/sys-quota-exceeded-back
Reviewer Review Type Date Requested Status
Facundo Batista (community) Approve
John O'Brien (community) Approve
Review via email: mp+51808@code.launchpad.net

Commit message

Bring SYS_QUOTA_EXCEEDED back (LP: #720797)

Description of the change

Bring SYS_QUOTA_EXCEEDED back

Commands now have a handle_retryable, a method they can use to receive a failure that does not make the command end, but to retry it.

The only one using it ATM is Upload, to send SYS_QUOTA_EXCEEDED.

Also, when testing this I noticed that if a command got stuck because of conditions, it never released the pathlock (even if cancelled!). So fixed that.

Tests included for everything.

To post a comment you must log in.
Revision history for this message
John O'Brien (jdobrien) wrote :

I love proposals that have more tests than code and find & fix a few bugs on the way.

review: Approve
Revision history for this message
Facundo Batista (facundo) wrote :

Approving with one review

review: Approve
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :

Attempt to merge into lp:ubuntuone-client failed due to conflicts:

text conflict in tests/syncdaemon/test_action_queue.py
text conflict in ubuntuone/syncdaemon/action_queue.py

906. By Facundo Batista

Merged trunk in

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'tests/syncdaemon/test_action_queue.py'
2--- tests/syncdaemon/test_action_queue.py 2011-03-02 14:24:13 +0000
3+++ tests/syncdaemon/test_action_queue.py 2011-03-02 19:30:59 +0000
4@@ -1936,10 +1936,11 @@
5 self.assertEqual(called, [exc, 1])
6
7 def test_run_enderr_retry(self):
8- """Command retried, don't call finish and retry."""
9+ """Command retried, call the handle and retry."""
10 called = []
11- self.cmd.finish = lambda: called.append(True)
12- self.cmd.handle_failure = lambda: called.append(True)
13+ self.cmd.finish = lambda: called.append('should not')
14+ self.cmd.handle_failure = lambda: called.append('should not')
15+ self.cmd.handle_retryable = lambda f: called.append('ok')
16 self.cmd.markers_resolved_deferred = defer.succeed(True)
17 assert self.rq.active
18
19@@ -1953,7 +1954,7 @@
20
21 # run and check finish was not called
22 self.cmd.run()
23- self.assertFalse(called)
24+ self.assertEqual(called, ['ok'])
25
26 def test_run_retry_on_commandpaused(self):
27 """Command retried because of pausing."""
28@@ -2048,6 +2049,7 @@
29 # nothing called when no deferred
30 assert self.cmd.wait_for_queue is None
31 self.cmd.resume()
32+ self.assertFalse(self.handler.check_debug('resuming'))
33
34 # the deferred is triggered if there
35 d = defer.Deferred()
36@@ -2055,12 +2057,14 @@
37 self.cmd.resume()
38 self.assertIdentical(self.cmd.wait_for_queue, None)
39 self.assertTrue(d.called)
40+ self.assertTrue(self.handler.check_debug('resuming'))
41
42 def test_check_conditions(self):
43 """Trigger the deferred only if there."""
44 # nothing called when no deferred
45 assert self.cmd.wait_for_conditions is None
46 self.cmd.check_conditions()
47+ self.assertFalse(self.handler.check_debug('unblocking conditions'))
48
49 # the deferred is triggered if there
50 d = defer.Deferred()
51@@ -2068,6 +2072,7 @@
52 self.cmd.check_conditions()
53 self.assertIdentical(self.cmd.wait_for_conditions, None)
54 self.assertTrue(d.called)
55+ self.assertTrue(self.handler.check_debug('unblocking conditions'))
56
57 def test_cancel_works(self):
58 """Do default cleaning."""
59@@ -2079,6 +2084,14 @@
60 self.assertEqual(called, [1, 2])
61 self.assertTrue(self.cmd.cancelled)
62
63+ def test_cancel_releases_conditions(self):
64+ """Cancel unlocks the conditions deferred."""
65+ self.cmd.finish = lambda: None # don't try to unqueue!
66+ d = defer.Deferred()
67+ self.cmd.wait_for_conditions = d
68+ self.cmd.cancel()
69+ self.assertTrue(d.called)
70+
71 def test_cancel_cancelled(self):
72 """Don't do anything if command already cancelled."""
73 called = []
74@@ -3128,7 +3141,7 @@
75 self.command.handle_failure(failure=failure)
76 self.assertFalse(os.path.exists(self.command.tempfile.name))
77
78- def test_handle_failure_push_quota_exceeded_if_quota_exceeded_error(self):
79+ def test_retryable_failure_push_quota_exceeded_if_that_error(self):
80 """Test SYS_QUOTA_EXCEEDED is pushed on QuotaExceededError."""
81 protocol_msg = protocol_pb2.Message()
82 protocol_msg.type = protocol_pb2.Message.ERROR
83@@ -3138,15 +3151,18 @@
84 error = errors.QuotaExceededError("request", protocol_msg)
85 failure = Failure(error)
86
87- self.command.handle_failure(failure=failure)
88- volume_id = self.command.share_id
89- kwargs = dict(share_id=volume_id, node_id='a_node_id',
90- hash='yadda', error=str(error))
91- event1 = ('SYS_QUOTA_EXCEEDED',
92- {'volume_id': volume_id, 'free_bytes': 1331564676})
93- event2 = ('AQ_UPLOAD_ERROR', kwargs)
94- self.assertTrue(event1 in self.command.action_queue.event_queue.events)
95- self.assertTrue(event2 in self.command.action_queue.event_queue.events)
96+ self.command.handle_retryable(failure)
97+ event = ('SYS_QUOTA_EXCEEDED', {'volume_id': self.command.share_id,
98+ 'free_bytes': 1331564676})
99+ self.assertTrue(event in self.command.action_queue.event_queue.events)
100+
101+ def test_retryable_failure_nothing_on_other_errors(self):
102+ """Test nothing is pushed on other errors."""
103+ failure = Failure(twisted_error.ConnectionLost())
104+ self.command.handle_retryable(failure)
105+ event_names = [x[0]
106+ for x in self.command.action_queue.event_queue.events]
107+ self.assertFalse('SYS_QUOTA_EXCEEDED' in event_names)
108
109 def test_AQ_UPLOAD_FILE_PROGRESS_is_valid_event(self):
110 """AQ_UPLOAD_FILE_PROGRESS is a valid event."""
111@@ -4871,9 +4887,10 @@
112 """Retry the command immediately."""
113 finished = defer.Deferred()
114 called = []
115- failure = Failure(twisted_error.ConnectionDone()) # retryable!
116- run_deferreds = [defer.fail(failure), defer.succeed('finish')]
117+ exc = twisted_error.ConnectionDone() # retryable!
118+ run_deferreds = [defer.fail(Failure(exc)), defer.succeed('finish')]
119 self.cmd._run = lambda: called.append('run') or run_deferreds.pop(0)
120+ self.cmd.handle_retryable = lambda f: called.append(f.value)
121
122 # check handle success (failure is never called because it's retried)
123 self.cmd.handle_success = lambda a: called.append(a)
124@@ -4892,7 +4909,7 @@
125 yield finished
126
127 # all check
128- self.assertEqual(called, ['run', 'run', 'finish'])
129+ self.assertEqual(called, ['run', exc, 'run', 'finish'])
130 self._check_finished_ok()
131
132 def test_retry_conditions_solved(self):
133@@ -4985,6 +5002,23 @@
134 self.assertEqual(called, [1])
135 self._check_finished_ok()
136
137+ def test_cancel_while_waiting_conditions(self):
138+ """Cancel the command while waiting for conditions."""
139+ # make it not runnable, and fake the pathlock to test releasing
140+ self.cmd.is_runnable = False
141+ released = []
142+ self.cmd._acquire_pathlock = lambda: defer.succeed(
143+ lambda: released.append(True))
144+
145+ # let the command go (will stuck because not runnable), and
146+ # cancel in the middle
147+ self.cmd.go()
148+ self.cmd.cancel()
149+
150+ # all check
151+ self._check_finished_ok()
152+ self.assertTrue(released)
153+
154 def test_marker_error_while_pathclocked(self):
155 """The marker errbacks while the command is waiting the pathlock."""
156 # monkeypatch methods to flag called and test "while running"
157
158=== modified file 'ubuntuone/syncdaemon/action_queue.py'
159--- ubuntuone/syncdaemon/action_queue.py 2011-03-02 14:24:13 +0000
160+++ ubuntuone/syncdaemon/action_queue.py 2011-03-02 19:30:59 +0000
161@@ -1201,12 +1201,14 @@
162 def resume(self):
163 """Unlock the command because the queue is back alive."""
164 if self.wait_for_queue is not None:
165+ self.log.debug('resuming')
166 self.wait_for_queue.callback(True)
167 self.wait_for_queue = None
168
169 def check_conditions(self):
170 """If conditions are ok, run the command again."""
171 if self.is_runnable and self.wait_for_conditions is not None:
172+ self.log.debug('unblocking conditions')
173 self.wait_for_conditions.callback(True)
174 self.wait_for_conditions = None
175
176@@ -1295,6 +1297,7 @@
177
178 if exc.__class__ in self.retryable_errors:
179 self.log.debug('retrying')
180+ self.handle_retryable(Failure(exc))
181 continue
182 else:
183 self.handle_failure(Failure(exc))
184@@ -1312,13 +1315,21 @@
185 def cancel(self):
186 """Cancel the command.
187
188+ It also triggers the wait_for_condition deferred (but not the
189+ wait_for_queue one, as if the queue is not active, it's pointless
190+ to release the pathlock for other commands).
191+
192 Do nothing if already cancelled (as cancellation can come from other
193 thread, it can come at any time, so we need to support double
194 cancellation safely).
195 """
196 if self.cancelled:
197 return
198+
199 self.cancelled = True
200+ if self.wait_for_conditions is not None:
201+ self.wait_for_conditions.callback(True)
202+ self.wait_for_conditions = None
203 self.cleanup()
204 self.finish()
205
206@@ -1332,6 +1343,9 @@
207 def handle_failure(self, failure):
208 """Do anthing that's needed to handle failure of the operation."""
209
210+ def handle_retryable(self, failure):
211+ """Had that failure, but the command will be retried."""
212+
213 def __str__(self, str_attrs=None):
214 """Return a str representation of the instance."""
215 if str_attrs is None:
216@@ -2416,17 +2430,19 @@
217 new_generation=request.new_generation)
218 self.action_queue.event_queue.push('AQ_UPLOAD_FINISHED', **d)
219
220- def handle_failure(self, failure):
221- """It didn't work! Push the event."""
222- if getattr(self.tempfile, 'name', None) is not None:
223- os.unlink(self.tempfile.name)
224-
225+ def handle_retryable(self, failure):
226+ """For a retryable failure."""
227 if failure.check(protocol_errors.QuotaExceededError):
228 error = failure.value
229 self.action_queue.event_queue.push('SYS_QUOTA_EXCEEDED',
230 volume_id=str(error.share_id),
231 free_bytes=error.free_bytes)
232
233+ def handle_failure(self, failure):
234+ """It didn't work! Push the event."""
235+ if getattr(self.tempfile, 'name', None) is not None:
236+ os.unlink(self.tempfile.name)
237+
238 self.action_queue.event_queue.push('AQ_UPLOAD_ERROR',
239 error=failure.getErrorMessage(),
240 share_id=self.share_id,

Subscribers

People subscribed via source and target branches