PageRenderTime 60ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 0ms

/python/google/appengine/tools/devappserver2/wsgi_server.py

https://gitlab.com/gregtyka/frankenserver
Python | 421 lines | 263 code | 46 blank | 112 comment | 47 complexity | 6a6b960521abf6990c9ffd5be727a151 MD5 | raw file
  1. #!/usr/bin/env python
  2. #
  3. # Copyright 2007 Google Inc.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. """A WSGI server implementation using a shared thread pool."""
  18. import collections
  19. import errno
  20. import httplib
  21. import logging
  22. import select
  23. import socket
  24. import threading
  25. import time
  26. import google
  27. from cherrypy import wsgiserver
  28. from google.appengine.tools.devappserver2 import errors
  29. from google.appengine.tools.devappserver2 import http_runtime_constants
  30. from google.appengine.tools.devappserver2 import shutdown
  31. from google.appengine.tools.devappserver2 import thread_executor
  32. _HAS_POLL = hasattr(select, 'poll')
  33. # TODO: the only reason we need to timeout is to pick up added or remove
  34. # descriptors. But AFAICT, we only add descriptors at startup and remove them at
  35. # shutdown so for the bulk of the run, the timeout is useless and just simply
  36. # wastes CPU. For startup, if we wait to start the thread until after all
  37. # WSGI servers are created, we are good (although may need to be careful in the
  38. # runtime instances depending on when servers are created relative to the
  39. # sandbox being enabled). For shutdown, more research is needed (one idea is
  40. # simply not remove descriptors as the process is about to exit).
  41. _READINESS_TIMEOUT_SECONDS = 1
  42. _SECONDS_TO_MILLISECONDS = 1000
  43. # Due to reports of failure to find a consistent port, trying a higher value
  44. # to see if that reduces the problem sufficiently. If it doesn't we can try
  45. # increasing it (on my circa 2010 desktop, it takes about 1/2 second per 1024
  46. # tries) but it would probably be better to either figure out a better
  47. # algorithm or make it possible for code to work with inconsistent ports.
  48. _PORT_0_RETRIES = 2048
  49. class BindError(errors.Error):
  50. """The server failed to bind its address."""
  51. _THREAD_POOL = thread_executor.ThreadExecutor()
  52. class _SharedCherryPyThreadPool(object):
  53. """A mimic of wsgiserver.ThreadPool that delegates to a shared thread pool."""
  54. def __init__(self):
  55. self._condition = threading.Condition()
  56. self._connections = set() # Protected by self._condition.
  57. def stop(self, timeout=5):
  58. timeout_time = time.time() + timeout
  59. with self._condition:
  60. while self._connections and time.time() < timeout_time:
  61. self._condition.wait(timeout_time - time.time())
  62. for connection in self._connections:
  63. self._shutdown_connection(connection)
  64. @staticmethod
  65. def _shutdown_connection(connection):
  66. if not connection.rfile.closed:
  67. connection.socket.shutdown(socket.SHUT_RD)
  68. def put(self, obj):
  69. with self._condition:
  70. self._connections.add(obj)
  71. _THREAD_POOL.submit(self._handle, obj)
  72. def _handle(self, obj):
  73. try:
  74. obj.communicate()
  75. finally:
  76. obj.close()
  77. with self._condition:
  78. self._connections.remove(obj)
  79. self._condition.notify()
  80. class SelectThread(object):
  81. """A thread that selects on sockets and calls corresponding callbacks."""
  82. def __init__(self):
  83. self._lock = threading.Lock()
  84. # self._file_descriptors is a frozenset and
  85. # self._file_descriptor_to_callback is never mutated so they can be
  86. # snapshotted by the select thread without needing to copy.
  87. self._file_descriptors = frozenset()
  88. self._file_descriptor_to_callback = {}
  89. self._select_thread = threading.Thread(
  90. target=self._loop_forever, name='WSGI select')
  91. self._select_thread.daemon = True
  92. def start(self):
  93. self._select_thread.start()
  94. def add_socket(self, s, callback):
  95. """Add a new socket to watch.
  96. Args:
  97. s: A socket to select on.
  98. callback: A callable with no args to be called when s is ready for a read.
  99. """
  100. with self._lock:
  101. self._file_descriptors = self._file_descriptors.union([s.fileno()])
  102. new_file_descriptor_to_callback = self._file_descriptor_to_callback.copy()
  103. new_file_descriptor_to_callback[s.fileno()] = callback
  104. self._file_descriptor_to_callback = new_file_descriptor_to_callback
  105. def remove_socket(self, s):
  106. """Remove a watched socket."""
  107. with self._lock:
  108. self._file_descriptors = self._file_descriptors.difference([s.fileno()])
  109. new_file_descriptor_to_callback = self._file_descriptor_to_callback.copy()
  110. del new_file_descriptor_to_callback[s.fileno()]
  111. self._file_descriptor_to_callback = new_file_descriptor_to_callback
  112. def _loop_forever(self):
  113. while shutdown and not shutdown.shutting_down():
  114. # Check shutdown as it may be gc-ed during shutdown. See
  115. # http://stackoverflow.com/questions/17084260/imported-modules-become-none-when-running-a-function
  116. self._select()
  117. def _select(self):
  118. with self._lock:
  119. fds = self._file_descriptors
  120. fd_to_callback = self._file_descriptor_to_callback
  121. if fds:
  122. if _HAS_POLL:
  123. # With 100 file descriptors, it is approximately 5x slower to
  124. # recreate and reinitialize the Poll object on every call to _select
  125. # rather reuse one. But the absolute cost of contruction,
  126. # initialization and calling poll(0) is ~25us so code simplicity
  127. # wins.
  128. poll = select.poll()
  129. for fd in fds:
  130. poll.register(fd, select.POLLIN)
  131. ready_file_descriptors = [fd for fd, _ in poll.poll(
  132. _READINESS_TIMEOUT_SECONDS * _SECONDS_TO_MILLISECONDS)]
  133. else:
  134. ready_file_descriptors, _, _ = select.select(fds, [], [],
  135. _READINESS_TIMEOUT_SECONDS)
  136. for fd in ready_file_descriptors:
  137. fd_to_callback[fd]()
  138. else:
  139. # select([], [], [], 1) is not supported on Windows.
  140. time.sleep(_READINESS_TIMEOUT_SECONDS)
  141. _SELECT_THREAD = SelectThread()
  142. _SELECT_THREAD.start()
  143. class _SingleAddressWsgiServer(wsgiserver.CherryPyWSGIServer):
  144. """A WSGI server that uses a shared SelectThread and thread pool."""
  145. def __init__(self, host, app):
  146. """Constructs a _SingleAddressWsgiServer.
  147. Args:
  148. host: A (hostname, port) tuple containing the hostname and port to bind.
  149. The port can be 0 to allow any port.
  150. app: A WSGI app to handle requests.
  151. """
  152. super(_SingleAddressWsgiServer, self).__init__(host, self)
  153. self._lock = threading.Lock()
  154. self._app = app # Protected by _lock.
  155. self._error = None # Protected by _lock.
  156. self.requests = _SharedCherryPyThreadPool()
  157. self.software = http_runtime_constants.SERVER_SOFTWARE
  158. # Some servers, especially the API server, may receive many simultaneous
  159. # requests so set the listen() backlog to something high to reduce the
  160. # likelihood of refused connections.
  161. self.request_queue_size = 100
  162. def start(self):
  163. """Starts the _SingleAddressWsgiServer.
  164. This is a modified version of the base class implementation. Changes:
  165. - Removed unused functionality (Unix domain socket and SSL support).
  166. - Raises BindError instead of socket.error.
  167. - Uses _SharedCherryPyThreadPool instead of wsgiserver.ThreadPool.
  168. - Calls _SELECT_THREAD.add_socket instead of looping forever.
  169. Raises:
  170. BindError: The address could not be bound.
  171. """
  172. # AF_INET or AF_INET6 socket
  173. # Get the correct address family for our host (allows IPv6 addresses)
  174. host, port = self.bind_addr
  175. try:
  176. info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
  177. socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
  178. except socket.gaierror:
  179. if ':' in host:
  180. info = [(socket.AF_INET6, socket.SOCK_STREAM, 0, '',
  181. self.bind_addr + (0, 0))]
  182. else:
  183. info = [(socket.AF_INET, socket.SOCK_STREAM, 0, '', self.bind_addr)]
  184. self.socket = None
  185. for res in info:
  186. af, socktype, proto, _, _ = res
  187. try:
  188. self.bind(af, socktype, proto)
  189. except socket.error as socket_error:
  190. if self.socket:
  191. self.socket.close()
  192. self.socket = None
  193. continue
  194. break
  195. if not self.socket:
  196. raise BindError('Unable to bind %s:%s' % self.bind_addr, socket_error)
  197. # Timeout so KeyboardInterrupt can be caught on Win32
  198. self.socket.settimeout(1)
  199. self.socket.listen(self.request_queue_size)
  200. self.ready = True
  201. self._start_time = time.time()
  202. _SELECT_THREAD.add_socket(self.socket, self.tick)
  203. def quit(self):
  204. """Quits the _SingleAddressWsgiServer."""
  205. _SELECT_THREAD.remove_socket(self.socket)
  206. self.requests.stop(timeout=1)
  207. @property
  208. def port(self):
  209. """Returns the port that the server is bound to."""
  210. return self.socket.getsockname()[1]
  211. def set_app(self, app):
  212. """Sets the PEP-333 app to use to serve requests."""
  213. with self._lock:
  214. self._app = app
  215. def set_error(self, error):
  216. """Sets the HTTP status code to serve for all requests."""
  217. with self._lock:
  218. self._error = error
  219. self._app = None
  220. def __call__(self, environ, start_response):
  221. with self._lock:
  222. app = self._app
  223. error = self._error
  224. if app:
  225. return app(environ, start_response)
  226. else:
  227. start_response('%d %s' % (error, httplib.responses[error]), [])
  228. return []
  229. class WsgiServer(object):
  230. def __init__(self, host, app):
  231. """Constructs a WsgiServer.
  232. Args:
  233. host: A (hostname, port) tuple containing the hostname and port to bind.
  234. The port can be 0 to allow any port.
  235. app: A WSGI app to handle requests.
  236. """
  237. self.bind_addr = host
  238. self._app = app
  239. self._servers = []
  240. def start(self):
  241. """Starts the WsgiServer.
  242. This starts multiple _SingleAddressWsgiServers to bind the address in all
  243. address families.
  244. Raises:
  245. BindError: The address could not be bound.
  246. """
  247. host, port = self.bind_addr
  248. try:
  249. addrinfo = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
  250. socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
  251. sockaddrs = [addr[-1] for addr in addrinfo]
  252. host_ports = [sockaddr[:2] for sockaddr in sockaddrs]
  253. # Remove duplicate addresses caused by bad hosts file. Retain the
  254. # order to minimize behavior change (and so we don't have to tweak
  255. # unit tests to deal with different order).
  256. host_ports = list(collections.OrderedDict.fromkeys(host_ports))
  257. except socket.gaierror:
  258. host_ports = [self.bind_addr]
  259. if port != 0:
  260. self._start_all_fixed_port(host_ports)
  261. else:
  262. for _ in range(_PORT_0_RETRIES):
  263. if self._start_all_dynamic_port(host_ports):
  264. break
  265. else:
  266. raise BindError('Unable to find a consistent port for %s' % host)
  267. def _start_all_fixed_port(self, host_ports):
  268. """Starts a server for each specified address with a fixed port.
  269. Does the work of actually trying to create a _SingleAddressWsgiServer for
  270. each specified address.
  271. Args:
  272. host_ports: An iterable of host, port tuples.
  273. Raises:
  274. BindError: The address could not be bound.
  275. """
  276. for host, port in host_ports:
  277. assert port != 0
  278. server = _SingleAddressWsgiServer((host, port), self._app)
  279. try:
  280. server.start()
  281. except BindError as bind_error:
  282. # TODO: I'm not sure about the behavior of quietly ignoring an
  283. # EADDRINUSE as long as the bind succeeds on at least one interface. I
  284. # think we should either:
  285. # - Fail (just like we do now when bind fails on every interface).
  286. # - Retry on next highest port.
  287. logging.debug('Failed to bind "%s:%s": %s', host, port, bind_error)
  288. continue
  289. else:
  290. self._servers.append(server)
  291. if not self._servers:
  292. raise BindError('Unable to bind %s:%s' % self.bind_addr)
  293. def _start_all_dynamic_port(self, host_ports):
  294. """Starts a server for each specified address with a dynamic port.
  295. Does the work of actually trying to create a _SingleAddressWsgiServer for
  296. each specified address.
  297. Args:
  298. host_ports: An iterable of host, port tuples.
  299. Returns:
  300. The list of all servers (also saved as self._servers). A non empty list
  301. indicates success while an empty list indicates failure.
  302. """
  303. port = 0
  304. for host, _ in host_ports:
  305. server = _SingleAddressWsgiServer((host, port), self._app)
  306. try:
  307. server.start()
  308. if port == 0:
  309. port = server.port
  310. except BindError as bind_error:
  311. if bind_error[1][0] == errno.EADDRINUSE:
  312. # The port picked at random for first interface was not available
  313. # on one of the other interfaces. Forget them and try again.
  314. for server in self._servers:
  315. server.quit()
  316. self._servers = []
  317. break
  318. else:
  319. # Ignore the interface if we get an error other than EADDRINUSE.
  320. logging.debug('Failed to bind "%s:%s": %s', host, port, bind_error)
  321. continue
  322. else:
  323. self._servers.append(server)
  324. return self._servers
  325. def quit(self):
  326. """Quits the WsgiServer."""
  327. for server in self._servers:
  328. server.quit()
  329. @property
  330. def host(self):
  331. """Returns the host that the server is bound to."""
  332. return self._servers[0].socket.getsockname()[0]
  333. @property
  334. def port(self):
  335. """Returns the port that the server is bound to."""
  336. return self._servers[0].socket.getsockname()[1]
  337. def set_app(self, app):
  338. """Sets the PEP-333 app to use to serve requests."""
  339. self._app = app
  340. for server in self._servers:
  341. server.set_app(app)
  342. def set_error(self, error):
  343. """Sets the HTTP status code to serve for all requests."""
  344. self._error = error
  345. self._app = None
  346. for server in self._servers:
  347. server.set_error(error)
  348. @property
  349. def ready(self):
  350. return all(server.ready for server in self._servers)