Mercurial > hg
comparison tests/badserverext.py @ 32001:c85f19c66e8d
tests: add tests for poorly behaving HTTP server
I've spent several hours over the past few weeks investigating
networking failures involving hg.mozilla.org. As part of this, it
has become clear that the Mercurial client's error handling when
it encounters network failures is far from robust.
To prove this is true, I've devised a battery of tests simulating
various network failures, notably premature connection closes. To
achieve this, I've implemented an extension that monkeypatches the
built-in HTTP server and hooks in at the socket level and allows
various events to occur based on config options. For example, you
can refuse to accept() a client socket or you can close() the socket
after N bytes have been sent or received. The latter effectively
simulates an unexpected connection drop (and these occur all the
time in the real world).
The new test file launches servers exhibiting various "bad" behaviors
and points a client at them. As the many TODO comments in the test
call attention to, Mercurial often displays unhelpful errors when
network-related failures occur. This makes it difficult for users
to understand what's going on and difficult for server administrators
to pinpoint root causes without packet tracing.
Upcoming patches will attempt to fix these error handling
deficiencies.
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Thu, 13 Apr 2017 22:19:28 -0700 |
parents | |
children | 08e46fcb8637 |
comparison
equal
deleted
inserted
replaced
32000:511a62669f1b | 32001:c85f19c66e8d |
---|---|
1 # badserverext.py - Extension making servers behave badly | |
2 # | |
3 # Copyright 2017 Gregory Szorc <gregory.szorc@gmail.com> | |
4 # | |
5 # This software may be used and distributed according to the terms of the | |
6 # GNU General Public License version 2 or any later version. | |
7 | |
8 # no-check-code | |
9 | |
10 """Extension to make servers behave badly. | |
11 | |
12 This extension is useful for testing Mercurial behavior when various network | |
13 events occur. | |
14 | |
15 Various config options in the [badserver] section influence behavior: | |
16 | |
17 closebeforeaccept | |
18 If true, close() the server socket when a new connection arrives before | |
19 accept() is called. The server will then exit. | |
20 | |
21 closeafteraccept | |
22 If true, the server will close() the client socket immediately after | |
23 accept(). | |
24 | |
25 closeafterrecvbytes | |
26 If defined, close the client socket after receiving this many bytes. | |
27 | |
28 closeaftersendbytes | |
29 If defined, close the client socket after sending this many bytes. | |
30 """ | |
31 | |
32 from __future__ import absolute_import | |
33 | |
34 import socket | |
35 | |
36 from mercurial.hgweb import ( | |
37 server, | |
38 ) | |
39 | |
40 # We can't adjust __class__ on a socket instance. So we define a proxy type. | |
41 class socketproxy(object): | |
42 __slots__ = ( | |
43 '_orig', | |
44 '_logfp', | |
45 '_closeafterrecvbytes', | |
46 '_closeaftersendbytes', | |
47 ) | |
48 | |
49 def __init__(self, obj, logfp, closeafterrecvbytes=0, | |
50 closeaftersendbytes=0): | |
51 object.__setattr__(self, '_orig', obj) | |
52 object.__setattr__(self, '_logfp', logfp) | |
53 object.__setattr__(self, '_closeafterrecvbytes', closeafterrecvbytes) | |
54 object.__setattr__(self, '_closeaftersendbytes', closeaftersendbytes) | |
55 | |
56 def __getattribute__(self, name): | |
57 if name in ('makefile',): | |
58 return object.__getattribute__(self, name) | |
59 | |
60 return getattr(object.__getattribute__(self, '_orig'), name) | |
61 | |
62 def __delattr__(self, name): | |
63 delattr(object.__getattribute__(self, '_orig'), name) | |
64 | |
65 def __setattr__(self, name, value): | |
66 setattr(object.__getattribute__(self, '_orig'), name, value) | |
67 | |
68 def makefile(self, mode, bufsize): | |
69 f = object.__getattribute__(self, '_orig').makefile(mode, bufsize) | |
70 | |
71 logfp = object.__getattribute__(self, '_logfp') | |
72 closeafterrecvbytes = object.__getattribute__(self, | |
73 '_closeafterrecvbytes') | |
74 closeaftersendbytes = object.__getattribute__(self, | |
75 '_closeaftersendbytes') | |
76 | |
77 return fileobjectproxy(f, logfp, | |
78 closeafterrecvbytes=closeafterrecvbytes, | |
79 closeaftersendbytes=closeaftersendbytes) | |
80 | |
81 # We can't adjust __class__ on socket._fileobject, so define a proxy. | |
82 class fileobjectproxy(object): | |
83 __slots__ = ( | |
84 '_orig', | |
85 '_logfp', | |
86 '_closeafterrecvbytes', | |
87 '_closeaftersendbytes', | |
88 ) | |
89 | |
90 def __init__(self, obj, logfp, closeafterrecvbytes=0, | |
91 closeaftersendbytes=0): | |
92 object.__setattr__(self, '_orig', obj) | |
93 object.__setattr__(self, '_logfp', logfp) | |
94 object.__setattr__(self, '_closeafterrecvbytes', closeafterrecvbytes) | |
95 object.__setattr__(self, '_closeaftersendbytes', closeaftersendbytes) | |
96 | |
97 def __getattribute__(self, name): | |
98 if name in ('read', 'readline', 'write', '_writelog'): | |
99 return object.__getattribute__(self, name) | |
100 | |
101 return getattr(object.__getattribute__(self, '_orig'), name) | |
102 | |
103 def __delattr__(self, name): | |
104 delattr(object.__getattribute__(self, '_orig'), name) | |
105 | |
106 def __setattr__(self, name, value): | |
107 setattr(object.__getattribute__(self, '_orig'), name, value) | |
108 | |
109 def _writelog(self, msg): | |
110 msg = msg.replace('\r', '\\r').replace('\n', '\\n') | |
111 | |
112 object.__getattribute__(self, '_logfp').write(msg) | |
113 object.__getattribute__(self, '_logfp').write('\n') | |
114 | |
115 def read(self, size=-1): | |
116 remaining = object.__getattribute__(self, '_closeafterrecvbytes') | |
117 | |
118 # No read limit. Call original function. | |
119 if not remaining: | |
120 result = object.__getattribute__(self, '_orig').read(size) | |
121 self._writelog('read(%d) -> (%d) (%s) %s' % (size, | |
122 len(result), | |
123 result)) | |
124 return result | |
125 | |
126 origsize = size | |
127 | |
128 if size < 0: | |
129 size = remaining | |
130 else: | |
131 size = min(remaining, size) | |
132 | |
133 result = object.__getattribute__(self, '_orig').read(size) | |
134 remaining -= len(result) | |
135 | |
136 self._writelog('read(%d from %d) -> (%d) %s' % ( | |
137 size, origsize, len(result), result)) | |
138 | |
139 object.__setattr__(self, '_closeafterrecvbytes', remaining) | |
140 | |
141 if remaining <= 0: | |
142 self._writelog('read limit reached, closing socket') | |
143 self._sock.close() | |
144 # This is the easiest way to abort the current request. | |
145 raise Exception('connection closed after receiving N bytes') | |
146 | |
147 return result | |
148 | |
149 def readline(self, size=-1): | |
150 remaining = object.__getattribute__(self, '_closeafterrecvbytes') | |
151 | |
152 # No read limit. Call original function. | |
153 if not remaining: | |
154 result = object.__getattribute__(self, '_orig').readline(size) | |
155 self._writelog('readline(%d) -> (%d) %s' % ( | |
156 size, len(result), result)) | |
157 return result | |
158 | |
159 origsize = size | |
160 | |
161 if size < 0: | |
162 size = remaining | |
163 else: | |
164 size = min(remaining, size) | |
165 | |
166 result = object.__getattribute__(self, '_orig').readline(size) | |
167 remaining -= len(result) | |
168 | |
169 self._writelog('readline(%d from %d) -> (%d) %s' % ( | |
170 size, origsize, len(result), result)) | |
171 | |
172 object.__setattr__(self, '_closeafterrecvbytes', remaining) | |
173 | |
174 if remaining <= 0: | |
175 self._writelog('read limit reached; closing socket') | |
176 self._sock.close() | |
177 # This is the easiest way to abort the current request. | |
178 raise Exception('connection closed after receiving N bytes') | |
179 | |
180 return result | |
181 | |
182 def write(self, data): | |
183 remaining = object.__getattribute__(self, '_closeaftersendbytes') | |
184 | |
185 # No byte limit on this operation. Call original function. | |
186 if not remaining: | |
187 self._writelog('write(%d) -> %s' % (len(data), data)) | |
188 result = object.__getattribute__(self, '_orig').write(data) | |
189 return result | |
190 | |
191 if len(data) > remaining: | |
192 newdata = data[0:remaining] | |
193 else: | |
194 newdata = data | |
195 | |
196 remaining -= len(newdata) | |
197 | |
198 self._writelog('write(%d from %d) -> (%d) %s' % ( | |
199 len(newdata), len(data), remaining, newdata)) | |
200 | |
201 result = object.__getattribute__(self, '_orig').write(newdata) | |
202 | |
203 object.__setattr__(self, '_closeaftersendbytes', remaining) | |
204 | |
205 if remaining <= 0: | |
206 self._writelog('write limit reached; closing socket') | |
207 self._sock.close() | |
208 raise Exception('connection closed after sending N bytes') | |
209 | |
210 return result | |
211 | |
212 def extsetup(ui): | |
213 # Change the base HTTP server class so various events can be performed. | |
214 # See SocketServer.BaseServer for how the specially named methods work. | |
215 class badserver(server.MercurialHTTPServer): | |
216 def __init__(self, ui, *args, **kwargs): | |
217 self._ui = ui | |
218 super(badserver, self).__init__(ui, *args, **kwargs) | |
219 | |
220 # Need to inherit object so super() works. | |
221 class badrequesthandler(self.RequestHandlerClass, object): | |
222 def send_header(self, name, value): | |
223 # Make headers deterministic to facilitate testing. | |
224 if name.lower() == 'date': | |
225 value = 'Fri, 14 Apr 2017 00:00:00 GMT' | |
226 elif name.lower() == 'server': | |
227 value = 'badhttpserver' | |
228 | |
229 return super(badrequesthandler, self).send_header(name, | |
230 value) | |
231 | |
232 self.RequestHandlerClass = badrequesthandler | |
233 | |
234 # Called to accept() a pending socket. | |
235 def get_request(self): | |
236 if self._ui.configbool('badserver', 'closebeforeaccept'): | |
237 self.socket.close() | |
238 | |
239 # Tells the server to stop processing more requests. | |
240 self.__shutdown_request = True | |
241 | |
242 # Simulate failure to stop processing this request. | |
243 raise socket.error('close before accept') | |
244 | |
245 if self._ui.configbool('badserver', 'closeafteraccept'): | |
246 request, client_address = super(badserver, self).get_request() | |
247 request.close() | |
248 raise socket.error('close after accept') | |
249 | |
250 return super(badserver, self).get_request() | |
251 | |
252 # Does heavy lifting of processing a request. Invokes | |
253 # self.finish_request() which calls self.RequestHandlerClass() which | |
254 # is a hgweb.server._httprequesthandler. | |
255 def process_request(self, socket, address): | |
256 # Wrap socket in a proxy if we need to count bytes. | |
257 closeafterrecvbytes = self._ui.configint('badserver', | |
258 'closeafterrecvbytes', 0) | |
259 closeaftersendbytes = self._ui.configint('badserver', | |
260 'closeaftersendbytes', 0) | |
261 | |
262 if closeafterrecvbytes or closeaftersendbytes: | |
263 socket = socketproxy(socket, self.errorlog, | |
264 closeafterrecvbytes=closeafterrecvbytes, | |
265 closeaftersendbytes=closeaftersendbytes) | |
266 | |
267 return super(badserver, self).process_request(socket, address) | |
268 | |
269 server.MercurialHTTPServer = badserver |