Merge lp:~mterry/duplicity/retry-decorator into lp:duplicity/0.6

Proposed by Michael Terry
Status: Merged
Approved by: Kenneth Loafman
Approved revision: 726
Merged at revision: 738
Proposed branch: lp:~mterry/duplicity/retry-decorator
Merge into: lp:duplicity/0.6
Prerequisite: lp:~mterry/duplicity/gio-name
Diff against target: 130 lines (+50/-29)
2 files modified
duplicity/backend.py (+18/-0)
duplicity/backends/giobackend.py (+32/-29)
To merge this branch: bzr merge lp:~mterry/duplicity/retry-decorator
Reviewer Review Type Date Requested Status
duplicity-team Pending
Review via email: mp+63571@code.launchpad.net

Description of the change

This branch adds a decorator to duplicity.backend for use by the backends if they like. Decorators were added in Python 2.4, so I believe it's safe to rely on.

I then started using the decorator in the giobackend. And finally (the real purpose of all this), I added the decorator to giobackend's list and delete operations.

This was motivated by bug 545486 where the user was getting the message "No data available" seemingly because there was a long gap between mounting the remote location and doing the first operation (list) due to scanning during a dry-run. The first list operation would fail, but if we did a second one, it would succeed.

Presumably GIO was reconnecting but still failing that first time. Seems like bad GIO design, but this works around it anyway. Plus, better to retry than not whenever we hit the backend. Some of the same issues with put/get can be had with list/delete.

I haven't converted any of the other backends to use the decorator, but it seems like it could reduce a bit of code.

To post a comment you must log in.
lp:~mterry/duplicity/retry-decorator updated
726. By Michael Terry

move logging code up to retry decorator; fixes use of 'n' variable where it doesn't belong

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'duplicity/backend.py'
--- duplicity/backend.py 2011-05-17 12:09:36 +0000
+++ duplicity/backend.py 2011-06-12 22:26:31 +0000
@@ -294,6 +294,24 @@
294 return parsed_url.geturl().replace(parsed_url.netloc, straight_netloc, 1)294 return parsed_url.geturl().replace(parsed_url.netloc, straight_netloc, 1)
295295
296296
297# Decorator for backend operation functions to simplify writing one that
298# retries. Make sure to add a keyword argument 'raise_errors' to your function
299# and if it is true, raise an exception on an error. If false, fatal-log it.
300def retry(fn):
301 def iterate(*args):
302 for n in range(1, globals.num_retries):
303 try:
304 return fn(*args, raise_errors=True)
305 except Exception, e:
306 log.Warn("Attempt %s failed: %s: %s"
307 % (n, e.__class__.__name__, str(e)))
308 log.Debug("Backtrace of previous error: %s"
309 % exception_traceback())
310 # Now try one last time, but fatal-log instead of raising errors
311 return fn(*args, raise_errors=False)
312 return iterate
313
314
297class Backend:315class Backend:
298 """316 """
299 Represents a generic duplicity backend, capable of storing and317 Represents a generic duplicity backend, capable of storing and
300318
=== modified file 'duplicity/backends/giobackend.py'
--- duplicity/backends/giobackend.py 2011-06-12 22:26:31 +0000
+++ duplicity/backends/giobackend.py 2011-06-12 22:26:31 +0000
@@ -27,6 +27,7 @@
27import glib #@UnresolvedImport27import glib #@UnresolvedImport
2828
29import duplicity.backend29import duplicity.backend
30from duplicity.backend import retry
30from duplicity import log31from duplicity import log
31from duplicity import globals32from duplicity import globals
32from duplicity import util33from duplicity import util
@@ -89,7 +90,9 @@
89 % str(e), log.ErrorCode.connection_failed)90 % str(e), log.ErrorCode.connection_failed)
90 loop.quit()91 loop.quit()
9192
92 def handle_error(self, e, op, file1=None, file2=None):93 def handle_error(self, raise_error, e, op, file1=None, file2=None):
94 if raise_error:
95 raise e
93 code = log.ErrorCode.backend_error96 code = log.ErrorCode.backend_error
94 if isinstance(e, gio.Error):97 if isinstance(e, gio.Error):
95 if e.code == gio.ERROR_PERMISSION_DENIED:98 if e.code == gio.ERROR_PERMISSION_DENIED:
@@ -105,21 +108,15 @@
105 def copy_progress(self, *args, **kwargs):108 def copy_progress(self, *args, **kwargs):
106 pass109 pass
107110
108 def copy_file(self, op, source, target):111 @retry
109 exc = None112 def copy_file(self, op, source, target, raise_errors=False):
110 for n in range(1, globals.num_retries+1):113 log.Info(_("Writing %s") % target.get_parse_name())
111 log.Info(_("Writing %s") % target.get_parse_name())114 try:
112 try:115 source.copy(target, self.copy_progress,
113 source.copy(target, self.copy_progress,116 gio.FILE_COPY_OVERWRITE | gio.FILE_COPY_NOFOLLOW_SYMLINKS)
114 gio.FILE_COPY_OVERWRITE | gio.FILE_COPY_NOFOLLOW_SYMLINKS)117 except Exception, e:
115 return118 self.handle_error(raise_errors, e, op, source.get_parse_name(),
116 except Exception, e:119 target.get_parse_name())
117 log.Warn("Write of '%s' failed (attempt %s): %s: %s"
118 % (target.get_parse_name(), n, e.__class__.__name__, str(e)))
119 log.Debug("Backtrace of previous error: %s"
120 % exception_traceback())
121 exc = e
122 self.handle_error(exc, op, source.get_parse_name(), target.get_parse_name())
123120
124 def put(self, source_path, remote_filename = None):121 def put(self, source_path, remote_filename = None):
125 """Copy file to remote"""122 """Copy file to remote"""
@@ -136,28 +133,34 @@
136 self.copy_file('get', source_file, target_file)133 self.copy_file('get', source_file, target_file)
137 local_path.setdata()134 local_path.setdata()
138135
139 def list(self):136 @retry
137 def list(self, raise_errors=False):
140 """List files in that directory"""138 """List files in that directory"""
139 files = []
141 try:140 try:
142 enum = self.remote_file.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_NAME,141 enum = self.remote_file.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_NAME,
143 gio.FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)142 gio.FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)
144 except Exception, e:
145 self.handle_error(e, 'list', self.remote_file.get_parse_name())
146 files = []
147 try:
148 info = enum.next_file()143 info = enum.next_file()
149 while info:144 while info:
150 files.append(info.get_name())145 files.append(info.get_name())
151 info = enum.next_file()146 info = enum.next_file()
152 return files
153 except Exception, e:147 except Exception, e:
154 self.handle_error(e, 'list')148 self.handle_error(raise_errors, e, 'list',
149 self.remote_file.get_parse_name())
150 return files
155151
156 def delete(self, filename_list):152 @retry
153 def delete(self, filename_list, raise_errors=False):
157 """Delete all files in filename list"""154 """Delete all files in filename list"""
158 assert type(filename_list) is not types.StringType155 assert type(filename_list) is not types.StringType
159 try:156 for filename in filename_list:
160 for filename in filename_list:157 target_file = self.remote_file.get_child(filename)
161 self.remote_file.get_child(filename).delete()158 try:
162 except Exception, e:159 target_file.delete()
163 self.handle_error(e, 'delete', self.remote_file.get_child(filename).get_parse_name())160 except Exception, e:
161 if isinstance(e, gio.Error):
162 if e.code == gio.ERROR_NOT_FOUND:
163 continue
164 self.handle_error(raise_errors, e, 'delete',
165 target_file.get_parse_name())
166 return

Subscribers

People subscribed via source and target branches

to all changes: