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
1=== modified file 'duplicity/backend.py'
2--- duplicity/backend.py 2011-05-17 12:09:36 +0000
3+++ duplicity/backend.py 2011-06-12 22:26:31 +0000
4@@ -294,6 +294,24 @@
5 return parsed_url.geturl().replace(parsed_url.netloc, straight_netloc, 1)
6
7
8+# Decorator for backend operation functions to simplify writing one that
9+# retries. Make sure to add a keyword argument 'raise_errors' to your function
10+# and if it is true, raise an exception on an error. If false, fatal-log it.
11+def retry(fn):
12+ def iterate(*args):
13+ for n in range(1, globals.num_retries):
14+ try:
15+ return fn(*args, raise_errors=True)
16+ except Exception, e:
17+ log.Warn("Attempt %s failed: %s: %s"
18+ % (n, e.__class__.__name__, str(e)))
19+ log.Debug("Backtrace of previous error: %s"
20+ % exception_traceback())
21+ # Now try one last time, but fatal-log instead of raising errors
22+ return fn(*args, raise_errors=False)
23+ return iterate
24+
25+
26 class Backend:
27 """
28 Represents a generic duplicity backend, capable of storing and
29
30=== modified file 'duplicity/backends/giobackend.py'
31--- duplicity/backends/giobackend.py 2011-06-12 22:26:31 +0000
32+++ duplicity/backends/giobackend.py 2011-06-12 22:26:31 +0000
33@@ -27,6 +27,7 @@
34 import glib #@UnresolvedImport
35
36 import duplicity.backend
37+from duplicity.backend import retry
38 from duplicity import log
39 from duplicity import globals
40 from duplicity import util
41@@ -89,7 +90,9 @@
42 % str(e), log.ErrorCode.connection_failed)
43 loop.quit()
44
45- def handle_error(self, e, op, file1=None, file2=None):
46+ def handle_error(self, raise_error, e, op, file1=None, file2=None):
47+ if raise_error:
48+ raise e
49 code = log.ErrorCode.backend_error
50 if isinstance(e, gio.Error):
51 if e.code == gio.ERROR_PERMISSION_DENIED:
52@@ -105,21 +108,15 @@
53 def copy_progress(self, *args, **kwargs):
54 pass
55
56- def copy_file(self, op, source, target):
57- exc = None
58- for n in range(1, globals.num_retries+1):
59- log.Info(_("Writing %s") % target.get_parse_name())
60- try:
61- source.copy(target, self.copy_progress,
62- gio.FILE_COPY_OVERWRITE | gio.FILE_COPY_NOFOLLOW_SYMLINKS)
63- return
64- except Exception, e:
65- log.Warn("Write of '%s' failed (attempt %s): %s: %s"
66- % (target.get_parse_name(), n, e.__class__.__name__, str(e)))
67- log.Debug("Backtrace of previous error: %s"
68- % exception_traceback())
69- exc = e
70- self.handle_error(exc, op, source.get_parse_name(), target.get_parse_name())
71+ @retry
72+ def copy_file(self, op, source, target, raise_errors=False):
73+ log.Info(_("Writing %s") % target.get_parse_name())
74+ try:
75+ source.copy(target, self.copy_progress,
76+ gio.FILE_COPY_OVERWRITE | gio.FILE_COPY_NOFOLLOW_SYMLINKS)
77+ except Exception, e:
78+ self.handle_error(raise_errors, e, op, source.get_parse_name(),
79+ target.get_parse_name())
80
81 def put(self, source_path, remote_filename = None):
82 """Copy file to remote"""
83@@ -136,28 +133,34 @@
84 self.copy_file('get', source_file, target_file)
85 local_path.setdata()
86
87- def list(self):
88+ @retry
89+ def list(self, raise_errors=False):
90 """List files in that directory"""
91+ files = []
92 try:
93 enum = self.remote_file.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_NAME,
94 gio.FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)
95- except Exception, e:
96- self.handle_error(e, 'list', self.remote_file.get_parse_name())
97- files = []
98- try:
99 info = enum.next_file()
100 while info:
101 files.append(info.get_name())
102 info = enum.next_file()
103- return files
104 except Exception, e:
105- self.handle_error(e, 'list')
106+ self.handle_error(raise_errors, e, 'list',
107+ self.remote_file.get_parse_name())
108+ return files
109
110- def delete(self, filename_list):
111+ @retry
112+ def delete(self, filename_list, raise_errors=False):
113 """Delete all files in filename list"""
114 assert type(filename_list) is not types.StringType
115- try:
116- for filename in filename_list:
117- self.remote_file.get_child(filename).delete()
118- except Exception, e:
119- self.handle_error(e, 'delete', self.remote_file.get_child(filename).get_parse_name())
120+ for filename in filename_list:
121+ target_file = self.remote_file.get_child(filename)
122+ try:
123+ target_file.delete()
124+ except Exception, e:
125+ if isinstance(e, gio.Error):
126+ if e.code == gio.ERROR_NOT_FOUND:
127+ continue
128+ self.handle_error(raise_errors, e, 'delete',
129+ target_file.get_parse_name())
130+ return

Subscribers

People subscribed via source and target branches

to all changes: