##// END OF EJS Templates
vcs-client: report more data about the call for better request tracking
super-admin -
r4887:ae398e61 default
parent child Browse files
Show More
@@ -1,186 +1,190 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2020 RhodeCode GmbH
3 # Copyright (C) 2014-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Various version Control System version lib (vcs) management abstraction layer
22 Various version Control System version lib (vcs) management abstraction layer
23 for Python. Build with server client architecture.
23 for Python. Build with server client architecture.
24 """
24 """
25 import atexit
25 import atexit
26 import logging
26 import logging
27 import urlparse
27 import urlparse
28 from cStringIO import StringIO
28 from cStringIO import StringIO
29
29
30 import rhodecode
30 import rhodecode
31 from rhodecode.lib.vcs.conf import settings
31 from rhodecode.lib.vcs.conf import settings
32 from rhodecode.lib.vcs.backends import get_vcs_instance, get_backend
32 from rhodecode.lib.vcs.backends import get_vcs_instance, get_backend
33 from rhodecode.lib.vcs.exceptions import (
33 from rhodecode.lib.vcs.exceptions import (
34 VCSError, RepositoryError, CommitError, VCSCommunicationError)
34 VCSError, RepositoryError, CommitError, VCSCommunicationError)
35
35
36 VERSION = (0, 5, 0, 'dev')
36 VERSION = (0, 5, 0, 'dev')
37
37
38 __version__ = '.'.join((str(each) for each in VERSION[:4]))
38 __version__ = '.'.join((str(each) for each in VERSION[:4]))
39
39
40 __all__ = [
40 __all__ = [
41 'get_version', 'get_vcs_instance', 'get_backend',
41 'get_version', 'get_vcs_instance', 'get_backend',
42 'VCSError', 'RepositoryError', 'CommitError', 'VCSCommunicationError'
42 'VCSError', 'RepositoryError', 'CommitError', 'VCSCommunicationError'
43 ]
43 ]
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47 # The pycurl library directly accesses C API functions and is not patched by
47 # The pycurl library directly accesses C API functions and is not patched by
48 # gevent. This will potentially lead to deadlocks due to incompatibility to
48 # gevent. This will potentially lead to deadlocks due to incompatibility to
49 # gevent. Therefore we check if gevent is active and import a gevent compatible
49 # gevent. Therefore we check if gevent is active and import a gevent compatible
50 # wrapper in that case.
50 # wrapper in that case.
51 try:
51 try:
52 from gevent import monkey
52 from gevent import monkey
53 if monkey.is_module_patched('__builtin__'):
53 if monkey.is_module_patched('__builtin__'):
54 import geventcurl as pycurl
54 import geventcurl as pycurl
55 log.debug('Using gevent comapatible pycurl: %s', pycurl)
55 log.debug('Using gevent comapatible pycurl: %s', pycurl)
56 else:
56 else:
57 import pycurl
57 import pycurl
58 except ImportError:
58 except ImportError:
59 import pycurl
59 import pycurl
60
60
61
61
62 def get_version():
62 def get_version():
63 """
63 """
64 Returns shorter version (digit parts only) as string.
64 Returns shorter version (digit parts only) as string.
65 """
65 """
66 return '.'.join((str(each) for each in VERSION[:3]))
66 return '.'.join((str(each) for each in VERSION[:3]))
67
67
68
68
69 def connect_http(server_and_port):
69 def connect_http(server_and_port):
70 from rhodecode.lib.vcs import connection, client_http
70 from rhodecode.lib.vcs import connection, client_http
71 from rhodecode.lib.middleware.utils import scm_app
71 from rhodecode.lib.middleware.utils import scm_app
72
72
73 session_factory = client_http.ThreadlocalSessionFactory()
73 session_factory = client_http.ThreadlocalSessionFactory()
74
74
75 connection.Git = client_http.RemoteVCSMaker(
75 connection.Git = client_http.RemoteVCSMaker(
76 server_and_port, '/git', 'git', session_factory)
76 server_and_port, '/git', 'git', session_factory)
77 connection.Hg = client_http.RemoteVCSMaker(
77 connection.Hg = client_http.RemoteVCSMaker(
78 server_and_port, '/hg', 'hg', session_factory)
78 server_and_port, '/hg', 'hg', session_factory)
79 connection.Svn = client_http.RemoteVCSMaker(
79 connection.Svn = client_http.RemoteVCSMaker(
80 server_and_port, '/svn', 'svn', session_factory)
80 server_and_port, '/svn', 'svn', session_factory)
81 connection.Service = client_http.ServiceConnection(
81 connection.Service = client_http.ServiceConnection(
82 server_and_port, '/_service', session_factory)
82 server_and_port, '/_service', session_factory)
83
83
84 scm_app.HG_REMOTE_WSGI = client_http.VcsHttpProxy(
84 scm_app.HG_REMOTE_WSGI = client_http.VcsHttpProxy(
85 server_and_port, '/proxy/hg')
85 server_and_port, '/proxy/hg')
86 scm_app.GIT_REMOTE_WSGI = client_http.VcsHttpProxy(
86 scm_app.GIT_REMOTE_WSGI = client_http.VcsHttpProxy(
87 server_and_port, '/proxy/git')
87 server_and_port, '/proxy/git')
88
88
89 @atexit.register
89 @atexit.register
90 def free_connection_resources():
90 def free_connection_resources():
91 connection.Git = None
91 connection.Git = None
92 connection.Hg = None
92 connection.Hg = None
93 connection.Svn = None
93 connection.Svn = None
94 connection.Service = None
94 connection.Service = None
95
95
96
96
97 def connect_vcs(server_and_port, protocol):
97 def connect_vcs(server_and_port, protocol):
98 """
98 """
99 Initializes the connection to the vcs server.
99 Initializes the connection to the vcs server.
100
100
101 :param server_and_port: str, e.g. "localhost:9900"
101 :param server_and_port: str, e.g. "localhost:9900"
102 :param protocol: str or "http"
102 :param protocol: str or "http"
103 """
103 """
104 if protocol == 'http':
104 if protocol == 'http':
105 connect_http(server_and_port)
105 connect_http(server_and_port)
106 else:
106 else:
107 raise Exception('Invalid vcs server protocol "{}"'.format(protocol))
107 raise Exception('Invalid vcs server protocol "{}"'.format(protocol))
108
108
109
109
110 class CurlSession(object):
110 class CurlSession(object):
111 """
111 """
112 Modeled so that it provides a subset of the requests interface.
112 Modeled so that it provides a subset of the requests interface.
113
113
114 This has been created so that it does only provide a minimal API for our
114 This has been created so that it does only provide a minimal API for our
115 needs. The parts which it provides are based on the API of the library
115 needs. The parts which it provides are based on the API of the library
116 `requests` which allows us to easily benchmark against it.
116 `requests` which allows us to easily benchmark against it.
117
117
118 Please have a look at the class :class:`requests.Session` when you extend
118 Please have a look at the class :class:`requests.Session` when you extend
119 it.
119 it.
120 """
120 """
121
121
122 def __init__(self):
122 def __init__(self):
123 curl = pycurl.Curl()
123 curl = pycurl.Curl()
124 # TODO: johbo: I did test with 7.19 of libcurl. This version has
124 # TODO: johbo: I did test with 7.19 of libcurl. This version has
125 # trouble with 100 - continue being set in the expect header. This
125 # trouble with 100 - continue being set in the expect header. This
126 # can lead to massive performance drops, switching it off here.
126 # can lead to massive performance drops, switching it off here.
127 curl.setopt(curl.HTTPHEADER, ["Expect:"])
127
128 curl.setopt(curl.TCP_NODELAY, True)
128 curl.setopt(curl.TCP_NODELAY, True)
129 curl.setopt(curl.PROTOCOLS, curl.PROTO_HTTP)
129 curl.setopt(curl.PROTOCOLS, curl.PROTO_HTTP)
130 curl.setopt(curl.USERAGENT, 'RhodeCode HTTP {}'.format(rhodecode.__version__))
130 curl.setopt(curl.USERAGENT, 'RhodeCode HTTP {}'.format(rhodecode.__version__))
131 curl.setopt(curl.SSL_VERIFYPEER, 0)
131 curl.setopt(curl.SSL_VERIFYPEER, 0)
132 curl.setopt(curl.SSL_VERIFYHOST, 0)
132 curl.setopt(curl.SSL_VERIFYHOST, 0)
133 self._curl = curl
133 self._curl = curl
134
134
135 def post(self, url, data, allow_redirects=False):
135 def post(self, url, data, allow_redirects=False, headers=None):
136 headers = headers or {}
137 # format is ['header_name1: header_value1', 'header_name2: header_value2'])
138 headers_list = ["Expect:"] + ['{}: {}'.format(k, v) for k, v in headers.items()]
136 response_buffer = StringIO()
139 response_buffer = StringIO()
137
140
138 curl = self._curl
141 curl = self._curl
139 curl.setopt(curl.URL, url)
142 curl.setopt(curl.URL, url)
140 curl.setopt(curl.POST, True)
143 curl.setopt(curl.POST, True)
141 curl.setopt(curl.POSTFIELDS, data)
144 curl.setopt(curl.POSTFIELDS, data)
142 curl.setopt(curl.FOLLOWLOCATION, allow_redirects)
145 curl.setopt(curl.FOLLOWLOCATION, allow_redirects)
143 curl.setopt(curl.WRITEDATA, response_buffer)
146 curl.setopt(curl.WRITEDATA, response_buffer)
147 curl.setopt(curl.HTTPHEADER, headers_list)
144 curl.perform()
148 curl.perform()
145
149
146 status_code = curl.getinfo(pycurl.HTTP_CODE)
150 status_code = curl.getinfo(pycurl.HTTP_CODE)
147
151
148 return CurlResponse(response_buffer, status_code)
152 return CurlResponse(response_buffer, status_code)
149
153
150
154
151 class CurlResponse(object):
155 class CurlResponse(object):
152 """
156 """
153 The response of a request, modeled after the requests API.
157 The response of a request, modeled after the requests API.
154
158
155 This class provides a subset of the response interface known from the
159 This class provides a subset of the response interface known from the
156 library `requests`. It is intentionally kept similar, so that we can use
160 library `requests`. It is intentionally kept similar, so that we can use
157 `requests` as a drop in replacement for benchmarking purposes.
161 `requests` as a drop in replacement for benchmarking purposes.
158 """
162 """
159
163
160 def __init__(self, response_buffer, status_code):
164 def __init__(self, response_buffer, status_code):
161 self._response_buffer = response_buffer
165 self._response_buffer = response_buffer
162 self._status_code = status_code
166 self._status_code = status_code
163
167
164 @property
168 @property
165 def content(self):
169 def content(self):
166 try:
170 try:
167 return self._response_buffer.getvalue()
171 return self._response_buffer.getvalue()
168 finally:
172 finally:
169 self._response_buffer.close()
173 self._response_buffer.close()
170
174
171 @property
175 @property
172 def status_code(self):
176 def status_code(self):
173 return self._status_code
177 return self._status_code
174
178
175 def iter_content(self, chunk_size):
179 def iter_content(self, chunk_size):
176 self._response_buffer.seek(0)
180 self._response_buffer.seek(0)
177 while 1:
181 while 1:
178 chunk = self._response_buffer.read(chunk_size)
182 chunk = self._response_buffer.read(chunk_size)
179 if not chunk:
183 if not chunk:
180 break
184 break
181 yield chunk
185 yield chunk
182
186
183
187
184 def _create_http_rpc_session():
188 def _create_http_rpc_session():
185 session = CurlSession()
189 session = CurlSession()
186 return session
190 return session
@@ -1,396 +1,408 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Client for the VCSServer implemented based on HTTP.
22 Client for the VCSServer implemented based on HTTP.
23 """
23 """
24
24
25 import copy
25 import copy
26 import logging
26 import logging
27 import threading
27 import threading
28 import time
28 import time
29 import urllib2
29 import urllib2
30 import urlparse
30 import urlparse
31 import uuid
31 import uuid
32 import traceback
32 import traceback
33
33
34 import pycurl
34 import pycurl
35 import msgpack
35 import msgpack
36 import requests
36 import requests
37 from requests.packages.urllib3.util.retry import Retry
37 from requests.packages.urllib3.util.retry import Retry
38
38
39 import rhodecode
39 import rhodecode
40 from rhodecode.lib import rc_cache
40 from rhodecode.lib import rc_cache
41 from rhodecode.lib.rc_cache.utils import compute_key_from_params
41 from rhodecode.lib.rc_cache.utils import compute_key_from_params
42 from rhodecode.lib.system_info import get_cert_path
42 from rhodecode.lib.system_info import get_cert_path
43 from rhodecode.lib.vcs import exceptions, CurlSession
43 from rhodecode.lib.vcs import exceptions, CurlSession
44 from rhodecode.lib.utils2 import str2bool
44 from rhodecode.lib.utils2 import str2bool
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 # TODO: mikhail: Keep it in sync with vcsserver's
49 # TODO: mikhail: Keep it in sync with vcsserver's
50 # HTTPApplication.ALLOWED_EXCEPTIONS
50 # HTTPApplication.ALLOWED_EXCEPTIONS
51 EXCEPTIONS_MAP = {
51 EXCEPTIONS_MAP = {
52 'KeyError': KeyError,
52 'KeyError': KeyError,
53 'URLError': urllib2.URLError,
53 'URLError': urllib2.URLError,
54 }
54 }
55
55
56
56
57 def _remote_call(url, payload, exceptions_map, session):
57 def _remote_call(url, payload, exceptions_map, session):
58 try:
58 try:
59 response = session.post(url, data=msgpack.packb(payload))
59 headers = {
60 'X-RC-Method': payload.get('method'),
61 'X-RC-Repo-Name': payload.get('_repo_name')
62 }
63 response = session.post(url, data=msgpack.packb(payload), headers=headers)
60 except pycurl.error as e:
64 except pycurl.error as e:
61 msg = '{}. \npycurl traceback: {}'.format(e, traceback.format_exc())
65 msg = '{}. \npycurl traceback: {}'.format(e, traceback.format_exc())
62 raise exceptions.HttpVCSCommunicationError(msg)
66 raise exceptions.HttpVCSCommunicationError(msg)
63 except Exception as e:
67 except Exception as e:
64 message = getattr(e, 'message', '')
68 message = getattr(e, 'message', '')
65 if 'Failed to connect' in message:
69 if 'Failed to connect' in message:
66 # gevent doesn't return proper pycurl errors
70 # gevent doesn't return proper pycurl errors
67 raise exceptions.HttpVCSCommunicationError(e)
71 raise exceptions.HttpVCSCommunicationError(e)
68 else:
72 else:
69 raise
73 raise
70
74
71 if response.status_code >= 400:
75 if response.status_code >= 400:
72 log.error('Call to %s returned non 200 HTTP code: %s',
76 log.error('Call to %s returned non 200 HTTP code: %s',
73 url, response.status_code)
77 url, response.status_code)
74 raise exceptions.HttpVCSCommunicationError(repr(response.content))
78 raise exceptions.HttpVCSCommunicationError(repr(response.content))
75
79
76 try:
80 try:
77 response = msgpack.unpackb(response.content)
81 response = msgpack.unpackb(response.content)
78 except Exception:
82 except Exception:
79 log.exception('Failed to decode response %r', response.content)
83 log.exception('Failed to decode response %r', response.content)
80 raise
84 raise
81
85
82 error = response.get('error')
86 error = response.get('error')
83 if error:
87 if error:
84 type_ = error.get('type', 'Exception')
88 type_ = error.get('type', 'Exception')
85 exc = exceptions_map.get(type_, Exception)
89 exc = exceptions_map.get(type_, Exception)
86 exc = exc(error.get('message'))
90 exc = exc(error.get('message'))
87 try:
91 try:
88 exc._vcs_kind = error['_vcs_kind']
92 exc._vcs_kind = error['_vcs_kind']
89 except KeyError:
93 except KeyError:
90 pass
94 pass
91
95
92 try:
96 try:
93 exc._vcs_server_traceback = error['traceback']
97 exc._vcs_server_traceback = error['traceback']
94 exc._vcs_server_org_exc_name = error['org_exc']
98 exc._vcs_server_org_exc_name = error['org_exc']
95 exc._vcs_server_org_exc_tb = error['org_exc_tb']
99 exc._vcs_server_org_exc_tb = error['org_exc_tb']
96 except KeyError:
100 except KeyError:
97 pass
101 pass
98
102
99 raise exc
103 raise exc
100 return response.get('result')
104 return response.get('result')
101
105
102
106
103 def _streaming_remote_call(url, payload, exceptions_map, session, chunk_size):
107 def _streaming_remote_call(url, payload, exceptions_map, session, chunk_size):
104 try:
108 try:
105 response = session.post(url, data=msgpack.packb(payload))
109 response = session.post(url, data=msgpack.packb(payload))
106 except pycurl.error as e:
110 except pycurl.error as e:
107 msg = '{}. \npycurl traceback: {}'.format(e, traceback.format_exc())
111 msg = '{}. \npycurl traceback: {}'.format(e, traceback.format_exc())
108 raise exceptions.HttpVCSCommunicationError(msg)
112 raise exceptions.HttpVCSCommunicationError(msg)
109 except Exception as e:
113 except Exception as e:
110 message = getattr(e, 'message', '')
114 message = getattr(e, 'message', '')
111 if 'Failed to connect' in message:
115 if 'Failed to connect' in message:
112 # gevent doesn't return proper pycurl errors
116 # gevent doesn't return proper pycurl errors
113 raise exceptions.HttpVCSCommunicationError(e)
117 raise exceptions.HttpVCSCommunicationError(e)
114 else:
118 else:
115 raise
119 raise
116
120
117 if response.status_code >= 400:
121 if response.status_code >= 400:
118 log.error('Call to %s returned non 200 HTTP code: %s',
122 log.error('Call to %s returned non 200 HTTP code: %s',
119 url, response.status_code)
123 url, response.status_code)
120 raise exceptions.HttpVCSCommunicationError(repr(response.content))
124 raise exceptions.HttpVCSCommunicationError(repr(response.content))
121
125
122 return response.iter_content(chunk_size=chunk_size)
126 return response.iter_content(chunk_size=chunk_size)
123
127
124
128
125 class ServiceConnection(object):
129 class ServiceConnection(object):
126 def __init__(self, server_and_port, backend_endpoint, session_factory):
130 def __init__(self, server_and_port, backend_endpoint, session_factory):
127 self.url = urlparse.urljoin('http://%s' % server_and_port, backend_endpoint)
131 self.url = urlparse.urljoin('http://%s' % server_and_port, backend_endpoint)
128 self._session_factory = session_factory
132 self._session_factory = session_factory
129
133
130 def __getattr__(self, name):
134 def __getattr__(self, name):
131 def f(*args, **kwargs):
135 def f(*args, **kwargs):
132 return self._call(name, *args, **kwargs)
136 return self._call(name, *args, **kwargs)
133 return f
137 return f
134
138
135 @exceptions.map_vcs_exceptions
139 @exceptions.map_vcs_exceptions
136 def _call(self, name, *args, **kwargs):
140 def _call(self, name, *args, **kwargs):
137 payload = {
141 payload = {
138 'id': str(uuid.uuid4()),
142 'id': str(uuid.uuid4()),
139 'method': name,
143 'method': name,
140 'params': {'args': args, 'kwargs': kwargs}
144 'params': {'args': args, 'kwargs': kwargs}
141 }
145 }
142 return _remote_call(
146 return _remote_call(
143 self.url, payload, EXCEPTIONS_MAP, self._session_factory())
147 self.url, payload, EXCEPTIONS_MAP, self._session_factory())
144
148
145
149
146 class RemoteVCSMaker(object):
150 class RemoteVCSMaker(object):
147
151
148 def __init__(self, server_and_port, backend_endpoint, backend_type, session_factory):
152 def __init__(self, server_and_port, backend_endpoint, backend_type, session_factory):
149 self.url = urlparse.urljoin('http://%s' % server_and_port, backend_endpoint)
153 self.url = urlparse.urljoin('http://%s' % server_and_port, backend_endpoint)
150 self.stream_url = urlparse.urljoin('http://%s' % server_and_port, backend_endpoint+'/stream')
154 self.stream_url = urlparse.urljoin('http://%s' % server_and_port, backend_endpoint+'/stream')
151
155
152 self._session_factory = session_factory
156 self._session_factory = session_factory
153 self.backend_type = backend_type
157 self.backend_type = backend_type
154
158
155 @classmethod
159 @classmethod
156 def init_cache_region(cls, repo_id):
160 def init_cache_region(cls, repo_id):
157 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
161 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
158 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
162 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
159 return region, cache_namespace_uid
163 return region, cache_namespace_uid
160
164
161 def __call__(self, path, repo_id, config, with_wire=None):
165 def __call__(self, path, repo_id, config, with_wire=None):
162 log.debug('%s RepoMaker call on %s', self.backend_type.upper(), path)
166 log.debug('%s RepoMaker call on %s', self.backend_type.upper(), path)
163 return RemoteRepo(path, repo_id, config, self, with_wire=with_wire)
167 return RemoteRepo(path, repo_id, config, self, with_wire=with_wire)
164
168
165 def __getattr__(self, name):
169 def __getattr__(self, name):
166 def remote_attr(*args, **kwargs):
170 def remote_attr(*args, **kwargs):
167 return self._call(name, *args, **kwargs)
171 return self._call(name, *args, **kwargs)
168 return remote_attr
172 return remote_attr
169
173
170 @exceptions.map_vcs_exceptions
174 @exceptions.map_vcs_exceptions
171 def _call(self, func_name, *args, **kwargs):
175 def _call(self, func_name, *args, **kwargs):
172 payload = {
176 payload = {
173 'id': str(uuid.uuid4()),
177 'id': str(uuid.uuid4()),
174 'method': func_name,
178 'method': func_name,
175 'backend': self.backend_type,
179 'backend': self.backend_type,
176 'params': {'args': args, 'kwargs': kwargs}
180 'params': {'args': args, 'kwargs': kwargs}
177 }
181 }
178 url = self.url
182 url = self.url
179 return _remote_call(url, payload, EXCEPTIONS_MAP, self._session_factory())
183 return _remote_call(url, payload, EXCEPTIONS_MAP, self._session_factory())
180
184
181
185
182 class RemoteRepo(object):
186 class RemoteRepo(object):
183 CHUNK_SIZE = 16384
187 CHUNK_SIZE = 16384
184
188
185 def __init__(self, path, repo_id, config, remote_maker, with_wire=None):
189 def __init__(self, path, repo_id, config, remote_maker, with_wire=None):
186 self.url = remote_maker.url
190 self.url = remote_maker.url
187 self.stream_url = remote_maker.stream_url
191 self.stream_url = remote_maker.stream_url
188 self._session = remote_maker._session_factory()
192 self._session = remote_maker._session_factory()
189
193
190 cache_repo_id = self._repo_id_sanitizer(repo_id)
194 cache_repo_id = self._repo_id_sanitizer(repo_id)
195 _repo_name = self._get_repo_name(config, path)
191 self._cache_region, self._cache_namespace = \
196 self._cache_region, self._cache_namespace = \
192 remote_maker.init_cache_region(cache_repo_id)
197 remote_maker.init_cache_region(cache_repo_id)
193
198
194 with_wire = with_wire or {}
199 with_wire = with_wire or {}
195
200
196 repo_state_uid = with_wire.get('repo_state_uid') or 'state'
201 repo_state_uid = with_wire.get('repo_state_uid') or 'state'
202
197 self._wire = {
203 self._wire = {
204 "_repo_name": _repo_name,
198 "path": path, # repo path
205 "path": path, # repo path
199 "repo_id": repo_id,
206 "repo_id": repo_id,
200 "cache_repo_id": cache_repo_id,
207 "cache_repo_id": cache_repo_id,
201 "config": config,
208 "config": config,
202 "repo_state_uid": repo_state_uid,
209 "repo_state_uid": repo_state_uid,
203 "context": self._create_vcs_cache_context(path, repo_state_uid)
210 "context": self._create_vcs_cache_context(path, repo_state_uid)
204 }
211 }
205
212
206 if with_wire:
213 if with_wire:
207 self._wire.update(with_wire)
214 self._wire.update(with_wire)
208
215
209 # NOTE(johbo): Trading complexity for performance. Avoiding the call to
216 # NOTE(johbo): Trading complexity for performance. Avoiding the call to
210 # log.debug brings a few percent gain even if is is not active.
217 # log.debug brings a few percent gain even if is is not active.
211 if log.isEnabledFor(logging.DEBUG):
218 if log.isEnabledFor(logging.DEBUG):
212 self._call_with_logging = True
219 self._call_with_logging = True
213
220
214 self.cert_dir = get_cert_path(rhodecode.CONFIG.get('__file__'))
221 self.cert_dir = get_cert_path(rhodecode.CONFIG.get('__file__'))
215
222
223 def _get_repo_name(self, config, path):
224 repo_store = config.get('paths', '/')
225 return path.split(repo_store)[-1].lstrip('/')
226
216 def _repo_id_sanitizer(self, repo_id):
227 def _repo_id_sanitizer(self, repo_id):
217 pathless = repo_id.replace('/', '__').replace('-', '_')
228 pathless = repo_id.replace('/', '__').replace('-', '_')
218 return ''.join(char if ord(char) < 128 else '_{}_'.format(ord(char)) for char in pathless)
229 return ''.join(char if ord(char) < 128 else '_{}_'.format(ord(char)) for char in pathless)
219
230
220 def __getattr__(self, name):
231 def __getattr__(self, name):
221
232
222 if name.startswith('stream:'):
233 if name.startswith('stream:'):
223 def repo_remote_attr(*args, **kwargs):
234 def repo_remote_attr(*args, **kwargs):
224 return self._call_stream(name, *args, **kwargs)
235 return self._call_stream(name, *args, **kwargs)
225 else:
236 else:
226 def repo_remote_attr(*args, **kwargs):
237 def repo_remote_attr(*args, **kwargs):
227 return self._call(name, *args, **kwargs)
238 return self._call(name, *args, **kwargs)
228
239
229 return repo_remote_attr
240 return repo_remote_attr
230
241
231 def _base_call(self, name, *args, **kwargs):
242 def _base_call(self, name, *args, **kwargs):
232 # TODO: oliver: This is currently necessary pre-call since the
243 # TODO: oliver: This is currently necessary pre-call since the
233 # config object is being changed for hooking scenarios
244 # config object is being changed for hooking scenarios
234 wire = copy.deepcopy(self._wire)
245 wire = copy.deepcopy(self._wire)
235 wire["config"] = wire["config"].serialize()
246 wire["config"] = wire["config"].serialize()
236 wire["config"].append(('vcs', 'ssl_dir', self.cert_dir))
247 wire["config"].append(('vcs', 'ssl_dir', self.cert_dir))
237
248
238 payload = {
249 payload = {
239 'id': str(uuid.uuid4()),
250 'id': str(uuid.uuid4()),
240 'method': name,
251 'method': name,
252 "_repo_name": wire['_repo_name'],
241 'params': {'wire': wire, 'args': args, 'kwargs': kwargs}
253 'params': {'wire': wire, 'args': args, 'kwargs': kwargs}
242 }
254 }
243
255
244 context_uid = wire.get('context')
256 context_uid = wire.get('context')
245 return context_uid, payload
257 return context_uid, payload
246
258
247 def get_local_cache(self, name, args):
259 def get_local_cache(self, name, args):
248 cache_on = False
260 cache_on = False
249 cache_key = ''
261 cache_key = ''
250 local_cache_on = str2bool(rhodecode.CONFIG.get('vcs.methods.cache'))
262 local_cache_on = str2bool(rhodecode.CONFIG.get('vcs.methods.cache'))
251
263
252 cache_methods = [
264 cache_methods = [
253 'branches', 'tags', 'bookmarks',
265 'branches', 'tags', 'bookmarks',
254 'is_large_file', 'is_binary',
266 'is_large_file', 'is_binary',
255 'fctx_size', 'stream:fctx_node_data', 'blob_raw_length',
267 'fctx_size', 'stream:fctx_node_data', 'blob_raw_length',
256 'node_history',
268 'node_history',
257 'revision', 'tree_items',
269 'revision', 'tree_items',
258 'ctx_list', 'ctx_branch', 'ctx_description',
270 'ctx_list', 'ctx_branch', 'ctx_description',
259 'bulk_request',
271 'bulk_request',
260 'assert_correct_path'
272 'assert_correct_path'
261 ]
273 ]
262
274
263 if local_cache_on and name in cache_methods:
275 if local_cache_on and name in cache_methods:
264 cache_on = True
276 cache_on = True
265 repo_state_uid = self._wire['repo_state_uid']
277 repo_state_uid = self._wire['repo_state_uid']
266 call_args = [a for a in args]
278 call_args = [a for a in args]
267 cache_key = compute_key_from_params(repo_state_uid, name, *call_args)
279 cache_key = compute_key_from_params(repo_state_uid, name, *call_args)
268
280
269 return cache_on, cache_key
281 return cache_on, cache_key
270
282
271 @exceptions.map_vcs_exceptions
283 @exceptions.map_vcs_exceptions
272 def _call(self, name, *args, **kwargs):
284 def _call(self, name, *args, **kwargs):
273 context_uid, payload = self._base_call(name, *args, **kwargs)
285 context_uid, payload = self._base_call(name, *args, **kwargs)
274 url = self.url
286 url = self.url
275
287
276 start = time.time()
288 start = time.time()
277 cache_on, cache_key = self.get_local_cache(name, args)
289 cache_on, cache_key = self.get_local_cache(name, args)
278
290
279 @self._cache_region.conditional_cache_on_arguments(
291 @self._cache_region.conditional_cache_on_arguments(
280 namespace=self._cache_namespace, condition=cache_on and cache_key)
292 namespace=self._cache_namespace, condition=cache_on and cache_key)
281 def remote_call(_cache_key):
293 def remote_call(_cache_key):
282 if self._call_with_logging:
294 if self._call_with_logging:
283 log.debug('Calling %s@%s with args:%.10240r. wire_context: %s cache_on: %s',
295 log.debug('Calling %s@%s with args:%.10240r. wire_context: %s cache_on: %s',
284 url, name, args, context_uid, cache_on)
296 url, name, args, context_uid, cache_on)
285 return _remote_call(url, payload, EXCEPTIONS_MAP, self._session)
297 return _remote_call(url, payload, EXCEPTIONS_MAP, self._session)
286
298
287 result = remote_call(cache_key)
299 result = remote_call(cache_key)
288 if self._call_with_logging:
300 if self._call_with_logging:
289 log.debug('Call %s@%s took: %.4fs. wire_context: %s',
301 log.debug('Call %s@%s took: %.4fs. wire_context: %s',
290 url, name, time.time()-start, context_uid)
302 url, name, time.time()-start, context_uid)
291 return result
303 return result
292
304
293 @exceptions.map_vcs_exceptions
305 @exceptions.map_vcs_exceptions
294 def _call_stream(self, name, *args, **kwargs):
306 def _call_stream(self, name, *args, **kwargs):
295 context_uid, payload = self._base_call(name, *args, **kwargs)
307 context_uid, payload = self._base_call(name, *args, **kwargs)
296 payload['chunk_size'] = self.CHUNK_SIZE
308 payload['chunk_size'] = self.CHUNK_SIZE
297 url = self.stream_url
309 url = self.stream_url
298
310
299 start = time.time()
311 start = time.time()
300 cache_on, cache_key = self.get_local_cache(name, args)
312 cache_on, cache_key = self.get_local_cache(name, args)
301
313
302 # Cache is a problem because this is a stream
314 # Cache is a problem because this is a stream
303 def streaming_remote_call(_cache_key):
315 def streaming_remote_call(_cache_key):
304 if self._call_with_logging:
316 if self._call_with_logging:
305 log.debug('Calling %s@%s with args:%.10240r. wire_context: %s cache_on: %s',
317 log.debug('Calling %s@%s with args:%.10240r. wire_context: %s cache_on: %s',
306 url, name, args, context_uid, cache_on)
318 url, name, args, context_uid, cache_on)
307 return _streaming_remote_call(url, payload, EXCEPTIONS_MAP, self._session, self.CHUNK_SIZE)
319 return _streaming_remote_call(url, payload, EXCEPTIONS_MAP, self._session, self.CHUNK_SIZE)
308
320
309 result = streaming_remote_call(cache_key)
321 result = streaming_remote_call(cache_key)
310 if self._call_with_logging:
322 if self._call_with_logging:
311 log.debug('Call %s@%s took: %.4fs. wire_context: %s',
323 log.debug('Call %s@%s took: %.4fs. wire_context: %s',
312 url, name, time.time()-start, context_uid)
324 url, name, time.time()-start, context_uid)
313 return result
325 return result
314
326
315 def __getitem__(self, key):
327 def __getitem__(self, key):
316 return self.revision(key)
328 return self.revision(key)
317
329
318 def _create_vcs_cache_context(self, *args):
330 def _create_vcs_cache_context(self, *args):
319 """
331 """
320 Creates a unique string which is passed to the VCSServer on every
332 Creates a unique string which is passed to the VCSServer on every
321 remote call. It is used as cache key in the VCSServer.
333 remote call. It is used as cache key in the VCSServer.
322 """
334 """
323 hash_key = '-'.join(map(str, args))
335 hash_key = '-'.join(map(str, args))
324 return str(uuid.uuid5(uuid.NAMESPACE_URL, hash_key))
336 return str(uuid.uuid5(uuid.NAMESPACE_URL, hash_key))
325
337
326 def invalidate_vcs_cache(self):
338 def invalidate_vcs_cache(self):
327 """
339 """
328 This invalidates the context which is sent to the VCSServer on every
340 This invalidates the context which is sent to the VCSServer on every
329 call to a remote method. It forces the VCSServer to create a fresh
341 call to a remote method. It forces the VCSServer to create a fresh
330 repository instance on the next call to a remote method.
342 repository instance on the next call to a remote method.
331 """
343 """
332 self._wire['context'] = str(uuid.uuid4())
344 self._wire['context'] = str(uuid.uuid4())
333
345
334
346
335 class VcsHttpProxy(object):
347 class VcsHttpProxy(object):
336
348
337 CHUNK_SIZE = 16384
349 CHUNK_SIZE = 16384
338
350
339 def __init__(self, server_and_port, backend_endpoint):
351 def __init__(self, server_and_port, backend_endpoint):
340 retries = Retry(total=5, connect=None, read=None, redirect=None)
352 retries = Retry(total=5, connect=None, read=None, redirect=None)
341
353
342 adapter = requests.adapters.HTTPAdapter(max_retries=retries)
354 adapter = requests.adapters.HTTPAdapter(max_retries=retries)
343 self.base_url = urlparse.urljoin('http://%s' % server_and_port, backend_endpoint)
355 self.base_url = urlparse.urljoin('http://%s' % server_and_port, backend_endpoint)
344 self.session = requests.Session()
356 self.session = requests.Session()
345 self.session.mount('http://', adapter)
357 self.session.mount('http://', adapter)
346
358
347 def handle(self, environment, input_data, *args, **kwargs):
359 def handle(self, environment, input_data, *args, **kwargs):
348 data = {
360 data = {
349 'environment': environment,
361 'environment': environment,
350 'input_data': input_data,
362 'input_data': input_data,
351 'args': args,
363 'args': args,
352 'kwargs': kwargs
364 'kwargs': kwargs
353 }
365 }
354 result = self.session.post(
366 result = self.session.post(
355 self.base_url, msgpack.packb(data), stream=True)
367 self.base_url, msgpack.packb(data), stream=True)
356 return self._get_result(result)
368 return self._get_result(result)
357
369
358 def _deserialize_and_raise(self, error):
370 def _deserialize_and_raise(self, error):
359 exception = Exception(error['message'])
371 exception = Exception(error['message'])
360 try:
372 try:
361 exception._vcs_kind = error['_vcs_kind']
373 exception._vcs_kind = error['_vcs_kind']
362 except KeyError:
374 except KeyError:
363 pass
375 pass
364 raise exception
376 raise exception
365
377
366 def _iterate(self, result):
378 def _iterate(self, result):
367 unpacker = msgpack.Unpacker()
379 unpacker = msgpack.Unpacker()
368 for line in result.iter_content(chunk_size=self.CHUNK_SIZE):
380 for line in result.iter_content(chunk_size=self.CHUNK_SIZE):
369 unpacker.feed(line)
381 unpacker.feed(line)
370 for chunk in unpacker:
382 for chunk in unpacker:
371 yield chunk
383 yield chunk
372
384
373 def _get_result(self, result):
385 def _get_result(self, result):
374 iterator = self._iterate(result)
386 iterator = self._iterate(result)
375 error = iterator.next()
387 error = iterator.next()
376 if error:
388 if error:
377 self._deserialize_and_raise(error)
389 self._deserialize_and_raise(error)
378
390
379 status = iterator.next()
391 status = iterator.next()
380 headers = iterator.next()
392 headers = iterator.next()
381
393
382 return iterator, status, headers
394 return iterator, status, headers
383
395
384
396
385 class ThreadlocalSessionFactory(object):
397 class ThreadlocalSessionFactory(object):
386 """
398 """
387 Creates one CurlSession per thread on demand.
399 Creates one CurlSession per thread on demand.
388 """
400 """
389
401
390 def __init__(self):
402 def __init__(self):
391 self._thread_local = threading.local()
403 self._thread_local = threading.local()
392
404
393 def __call__(self):
405 def __call__(self):
394 if not hasattr(self._thread_local, 'curl_session'):
406 if not hasattr(self._thread_local, 'curl_session'):
395 self._thread_local.curl_session = CurlSession()
407 self._thread_local.curl_session = CurlSession()
396 return self._thread_local.curl_session
408 return self._thread_local.curl_session
@@ -1,133 +1,135 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 import mock
23 import mock
24 import msgpack
24 import msgpack
25 import pytest
25 import pytest
26
26
27 from rhodecode.lib import vcs
27 from rhodecode.lib import vcs
28 from rhodecode.lib.vcs import client_http, exceptions
28 from rhodecode.lib.vcs import client_http, exceptions
29
29
30
30
31 def is_new_connection(logger, level, message):
31 def is_new_connection(logger, level, message):
32 return (
32 return (
33 logger == 'requests.packages.urllib3.connectionpool' and
33 logger == 'requests.packages.urllib3.connectionpool' and
34 message.startswith('Starting new HTTP'))
34 message.startswith('Starting new HTTP'))
35
35
36
36
37 @pytest.fixture()
37 @pytest.fixture()
38 def stub_session():
38 def stub_session():
39 """
39 """
40 Stub of `requests.Session()`.
40 Stub of `requests.Session()`.
41 """
41 """
42 session = mock.Mock()
42 session = mock.Mock()
43 post = session.post()
43 post = session.post()
44 post.content = msgpack.packb({})
44 post.content = msgpack.packb({})
45 post.status_code = 200
45 post.status_code = 200
46
46
47 session.reset_mock()
47 session.reset_mock()
48 return session
48 return session
49
49
50
50
51 @pytest.fixture()
51 @pytest.fixture()
52 def stub_fail_session():
52 def stub_fail_session():
53 """
53 """
54 Stub of `requests.Session()`.
54 Stub of `requests.Session()`.
55 """
55 """
56 session = mock.Mock()
56 session = mock.Mock()
57 post = session.post()
57 post = session.post()
58 post.content = msgpack.packb({'error': '500'})
58 post.content = msgpack.packb({'error': '500'})
59 post.status_code = 500
59 post.status_code = 500
60
60
61 session.reset_mock()
61 session.reset_mock()
62 return session
62 return session
63
63
64
64
65 @pytest.fixture()
65 @pytest.fixture()
66 def stub_session_factory(stub_session):
66 def stub_session_factory(stub_session):
67 """
67 """
68 Stub of `rhodecode.lib.vcs.client_http.ThreadlocalSessionFactory`.
68 Stub of `rhodecode.lib.vcs.client_http.ThreadlocalSessionFactory`.
69 """
69 """
70 session_factory = mock.Mock()
70 session_factory = mock.Mock()
71 session_factory.return_value = stub_session
71 session_factory.return_value = stub_session
72 return session_factory
72 return session_factory
73
73
74
74
75 @pytest.fixture()
75 @pytest.fixture()
76 def stub_session_failing_factory(stub_fail_session):
76 def stub_session_failing_factory(stub_fail_session):
77 """
77 """
78 Stub of `rhodecode.lib.vcs.client_http.ThreadlocalSessionFactory`.
78 Stub of `rhodecode.lib.vcs.client_http.ThreadlocalSessionFactory`.
79 """
79 """
80 session_factory = mock.Mock()
80 session_factory = mock.Mock()
81 session_factory.return_value = stub_fail_session
81 session_factory.return_value = stub_fail_session
82 return session_factory
82 return session_factory
83
83
84
84
85 def test_uses_persistent_http_connections(caplog, vcsbackend_hg):
85 def test_uses_persistent_http_connections(caplog, vcsbackend_hg):
86 repo = vcsbackend_hg.repo
86 repo = vcsbackend_hg.repo
87 remote_call = repo._remote.branches
87 remote_call = repo._remote.branches
88
88
89 with caplog.at_level(logging.INFO):
89 with caplog.at_level(logging.INFO):
90 for x in range(5):
90 for x in range(5):
91 remote_call(normal=True, closed=False)
91 remote_call(normal=True, closed=False)
92
92
93 new_connections = [
93 new_connections = [
94 r for r in caplog.record_tuples if is_new_connection(*r)]
94 r for r in caplog.record_tuples if is_new_connection(*r)]
95 assert len(new_connections) <= 1
95 assert len(new_connections) <= 1
96
96
97
97
98 def test_repo_maker_uses_session_for_classmethods(stub_session_factory):
98 def test_repo_maker_uses_session_for_classmethods(stub_session_factory):
99 repo_maker = client_http.RemoteVCSMaker(
99 repo_maker = client_http.RemoteVCSMaker(
100 'server_and_port', 'endpoint', 'test_dummy_scm', stub_session_factory)
100 'server_and_port', 'endpoint', 'test_dummy_scm', stub_session_factory)
101 repo_maker.example_call()
101 repo_maker.example_call()
102 stub_session_factory().post.assert_called_with(
102 stub_session_factory().post.assert_called_with(
103 'http://server_and_port/endpoint', data=mock.ANY)
103 'http://server_and_port/endpoint', data=mock.ANY,
104 headers={'X-RC-Method': 'example_call', 'X-RC-Repo-Name': None})
104
105
105
106
106 def test_repo_maker_uses_session_for_instance_methods(
107 def test_repo_maker_uses_session_for_instance_methods(
107 stub_session_factory, config):
108 stub_session_factory, config):
108 repo_maker = client_http.RemoteVCSMaker(
109 repo_maker = client_http.RemoteVCSMaker(
109 'server_and_port', 'endpoint', 'test_dummy_scm', stub_session_factory)
110 'server_and_port', 'endpoint', 'test_dummy_scm', stub_session_factory)
110 repo = repo_maker('stub_path', 'stub_repo_id', config)
111 repo = repo_maker('stub_path', 'stub_repo_id', config)
111 repo.example_call()
112 repo.example_call()
112 stub_session_factory().post.assert_called_with(
113 stub_session_factory().post.assert_called_with(
113 'http://server_and_port/endpoint', data=mock.ANY)
114 'http://server_and_port/endpoint', data=mock.ANY,
115 headers={'X-RC-Method': 'example_call', 'X-RC-Repo-Name': 'stub_path'})
114
116
115
117
116 @mock.patch('rhodecode.lib.vcs.client_http.ThreadlocalSessionFactory')
118 @mock.patch('rhodecode.lib.vcs.client_http.ThreadlocalSessionFactory')
117 @mock.patch('rhodecode.lib.vcs.connection')
119 @mock.patch('rhodecode.lib.vcs.connection')
118 def test_connect_passes_in_the_same_session(
120 def test_connect_passes_in_the_same_session(
119 connection, session_factory_class, stub_session):
121 connection, session_factory_class, stub_session):
120 session_factory = session_factory_class.return_value
122 session_factory = session_factory_class.return_value
121 session_factory.return_value = stub_session
123 session_factory.return_value = stub_session
122
124
123 vcs.connect_http('server_and_port')
125 vcs.connect_http('server_and_port')
124
126
125
127
126 def test_repo_maker_uses_session_that_throws_error(
128 def test_repo_maker_uses_session_that_throws_error(
127 stub_session_failing_factory, config):
129 stub_session_failing_factory, config):
128 repo_maker = client_http.RemoteVCSMaker(
130 repo_maker = client_http.RemoteVCSMaker(
129 'server_and_port', 'endpoint', 'test_dummy_scm', stub_session_failing_factory)
131 'server_and_port', 'endpoint', 'test_dummy_scm', stub_session_failing_factory)
130 repo = repo_maker('stub_path', 'stub_repo_id', config)
132 repo = repo_maker('stub_path', 'stub_repo_id', config)
131
133
132 with pytest.raises(exceptions.HttpVCSCommunicationError):
134 with pytest.raises(exceptions.HttpVCSCommunicationError):
133 repo.example_call()
135 repo.example_call()
General Comments 0
You need to be logged in to leave comments. Login now