1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
# Copyright 2009 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Client code for the branch filesystem endpoint.
This code talks to the internal XML-RPC server for the branch filesystem.
"""
__metaclass__ = type
__all__ = [
'BranchFileSystemClient',
'NotInCache',
]
import time
from twisted.internet import defer
from lp.code.interfaces.codehosting import BRANCH_TRANSPORT
from lp.services.twistedsupport import no_traceback_failures
class NotInCache(Exception):
"""Raised when we try to get a path from the cache that's not present."""
class BranchFileSystemClient:
"""Wrapper for some methods of the codehosting endpoint.
Instances of this class wrap the methods of the codehosting endpoint
required by the VFS code, specialized for a particular user.
The wrapper also caches the results of calls to translatePath in order to
avoid a large number of roundtrips. In the normal course of operation, our
Bazaar transport translates virtual paths to real paths on disk using this
client. It does this many, many times for a single Bazaar operation, so we
cache the results here.
"""
def __init__(self, codehosting_endpoint, user_id, expiry_time=None,
seen_new_branch_hook=None, _now=time.time):
"""Construct a caching codehosting_endpoint.
:param codehosting_endpoint: An XML-RPC proxy that implements
callRemote and returns Deferreds.
:param user_id: The database ID of the user who will be making these
requests. An integer.
:param expiry_time: If supplied, only cache the results of
translatePath for this many seconds. If not supplied, cache the
results of translatePath for as long as this instance exists.
:param seen_new_branch_hook: A callable that will be called with the
unique_name of each new branch that is accessed.
"""
self._codehosting_endpoint = codehosting_endpoint
self._cache = {}
self._user_id = user_id
self.expiry_time = expiry_time
self._now = _now
self.seen_new_branch_hook = seen_new_branch_hook
def _getMatchedPart(self, path, transport_tuple):
"""Return the part of 'path' that the endpoint actually matched."""
trailing_length = len(transport_tuple[2])
if trailing_length == 0:
matched_part = path
else:
matched_part = path[:-trailing_length]
return matched_part.rstrip('/')
def _addToCache(self, transport_tuple, path):
"""Cache the given 'transport_tuple' results for 'path'.
:return: the 'transport_tuple' as given, so we can use this as a
callback.
"""
(transport_type, data, trailing_path) = transport_tuple
matched_part = self._getMatchedPart(path, transport_tuple)
if transport_type == BRANCH_TRANSPORT:
if self.seen_new_branch_hook:
self.seen_new_branch_hook(matched_part.strip('/'))
self._cache[matched_part] = (transport_type, data, self._now())
return transport_tuple
def _getFromCache(self, path):
"""Get the cached 'transport_tuple' for 'path'."""
split_path = path.strip('/').split('/')
for object_path, value in self._cache.iteritems():
transport_type, data, inserted_time = value
split_object_path = object_path.strip('/').split('/')
# Do a segment-by-segment comparison. Python sucks, lists should
# also have startswith.
if split_path[:len(split_object_path)] == split_object_path:
if (self.expiry_time is not None
and self._now() > inserted_time + self.expiry_time):
del self._cache[object_path]
break
trailing_path = '/'.join(split_path[len(split_object_path):])
return (transport_type, data, trailing_path)
raise NotInCache(path)
def createBranch(self, branch_path):
"""Create a Launchpad `IBranch` in the database.
This raises any Faults that might be raised by the
codehosting_endpoint's `createBranch` method, so for more information
see `IBranchFileSystem.createBranch`.
:param branch_path: The path to the branch to create.
:return: A `Deferred` that fires the ID of the created branch.
"""
return self._codehosting_endpoint.callRemote(
'createBranch', self._user_id, branch_path)
def branchChanged(self, branch_id, stacked_on_url, last_revision_id,
control_string, branch_string, repository_string):
"""Mark a branch as needing to be mirrored.
:param branch_id: The database ID of the branch.
"""
return self._codehosting_endpoint.callRemote(
'branchChanged', self._user_id, branch_id, stacked_on_url,
last_revision_id, control_string, branch_string,
repository_string)
def translatePath(self, path):
"""Translate 'path'."""
try:
return defer.succeed(self._getFromCache(path))
except NotInCache:
deferred = self._codehosting_endpoint.callRemote(
'translatePath', self._user_id, path)
deferred.addCallback(no_traceback_failures(self._addToCache), path)
return deferred
|