~launchpad-pqm/launchpad/devel

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