diff --git a/sanic/config.py b/sanic/config.py index 81f766fb..72b050b8 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -21,4 +21,4 @@ class Config: ▀▀▄▄▀ """ REQUEST_MAX_SIZE = 100000000 # 100 megababies - KEEP_ALIVE_TIMEOUT = 60 # 60 seconds \ No newline at end of file + REQUEST_TIMEOUT = 60 # 60 seconds \ No newline at end of file diff --git a/sanic/sanic.py b/sanic/sanic.py index 3b0c530b..90d8da2d 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,9 +1,11 @@ from .config import Config from .exceptions import Handler -from .log import log +from .log import log, logging from .response import HTTPResponse from .router import Router from .server import serve +from .exceptions import ServerError +from inspect import isawaitable class Sanic: name = None @@ -25,14 +27,54 @@ class Sanic: return response - def handler(self, *args, **kwargs): + def exception(self, *args, **kwargs): def response(handler): self.error_handler.add(*args, **kwargs) return handler return response + async def handle_request(self, request, respond): + try: + handler = self.router.get(request) + if handler is None: + raise ServerError("'None' was returned while requesting a handler from the router") + + response = handler(request) + # Check if the handler is asynchronous + if isawaitable(response): + response = await response + + except Exception as e: + try: + response = self.error_handler.response(request, e) + except Exception as e: + if self.debug: + response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc())) + else: + response = HTTPResponse("An error occured while handling an error") + + respond(response) + + def run(self, host="127.0.0.1", port=8000, debug=False, on_start=None, on_stop=None): self.error_handler.debug=True self.debug = debug - return serve(sanic=self, host=host, port=port, debug=debug, on_start=on_start, on_stop=on_stop) \ No newline at end of file + + if debug: + log.setLevel(logging.DEBUG) + log.debug(self.config.LOGO) + + # Serve + log.info('Goin\' Fast @ {}:{}'.format(host, port)) + + return serve( + host=host, + port=port, + debug=debug, + on_start=on_start, + on_stop=on_stop, + request_handler=self.handle_request, + request_timeout=self.config.REQUEST_TIMEOUT, + request_max_size=self.config.REQUEST_MAX_SIZE, + ) \ No newline at end of file diff --git a/sanic/server.py b/sanic/server.py index 9ab65624..074a3b48 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,14 +1,5 @@ -import argparse -import sys import asyncio -import signal -import functools -import httptools -import logging from inspect import isawaitable -from ujson import loads as json_loads -from traceback import format_exc -from time import time import httptools try: @@ -16,40 +7,45 @@ try: except: async_loop = asyncio -from socket import * - from .log import log -from .exceptions import ServerError -from .response import HTTPResponse from .request import Request +class Signal: + stopped = False + class HttpProtocol(asyncio.Protocol): - __slots__ = ('loop', 'transport', # event loop, connection + __slots__ = ('loop', 'transport', 'connections', 'signal', # event loop, connection 'parser', 'request', 'url', 'headers', # request params - 'sanic', # router and config object - '_total_body_size', '_timeout_handler') # connection management + 'request_handler', 'request_timeout', 'request_max_size', # request config + '_total_request_size', '_timeout_handler') # connection management - def __init__(self, *, sanic, loop): + def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60, request_max_size=None): self.loop = loop self.transport = None self.request = None self.parser = None self.url = None self.headers = None - self.sanic = sanic + self.signal = signal + self.connections = connections + self.request_handler = request_handler + self.request_timeout = request_timeout + self.request_max_size = request_max_size self._total_request_size = 0 + self._timeout_handler = None - # -------------------------------------------- # + # -------------------------------------------- # # Connection # -------------------------------------------- # def connection_made(self, transport): - self._timeout_handler = self.loop.call_later(self.sanic.config.KEEP_ALIVE_TIMEOUT, self.connection_timeout) + self.connections[self] = True + self._timeout_handler = self.loop.call_later(self.request_timeout, self.connection_timeout) self.transport = transport - #TODO: handle connection timeout def connection_lost(self, exc): + del self.connections[self] self._timeout_handler.cancel() self.cleanup() @@ -63,7 +59,7 @@ class HttpProtocol(asyncio.Protocol): def data_received(self, data): # Check for the request itself getting too large and exceeding memory limits self._total_request_size += len(data) - if self._total_request_size > self.sanic.config.REQUEST_MAX_SIZE: + if self._total_request_size > self.request_max_size: return self.bail_out("Request too large ({}), connection closed".format(self._total_request_size)) # Create parser if this is the first time we're receiving data @@ -82,7 +78,7 @@ class HttpProtocol(asyncio.Protocol): self.url = url def on_header(self, name, value): - if name == 'Content-Length' and int(value) > self.sanic.config.REQUEST_MAX_SIZE: + if name == 'Content-Length' and int(value) > self.request_max_size: return self.bail_out("Request body too large ({}), connection closed".format(value)) self.headers.append((name, value.decode('utf-8'))) @@ -98,41 +94,16 @@ class HttpProtocol(asyncio.Protocol): def on_body(self, body): self.request.body = body def on_message_complete(self): - self.loop.create_task(self.get_response()) + self.loop.create_task(self.request_handler(self.request, self.write_response)) # -------------------------------------------- # # Responding # -------------------------------------------- # - async def get_response(self): - try: - handler = self.sanic.router.get(self.request) - if handler is None: - raise ServerError("'None' was returned while requesting a handler from the router") - - response = handler(self.request) - - # Check if the handler is asynchronous - if isawaitable(response): - response = await response - - except Exception as e: - try: - response = self.sanic.error_handler.response(self.request, e) - except Exception as e: - if self.sanic.debug: - response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc())) - else: - response = HTTPResponse("An error occured while handling an error") - - self.write_response(response) - def write_response(self, response): - #print("response - {} - {}".format(self.n, self.request)) try: - keep_alive = self.parser.should_keep_alive() - self.transport.write(response.output(self.request.version, keep_alive, self.sanic.config.KEEP_ALIVE_TIMEOUT)) - #print("KA - {}".format(self.parser.should_keep_alive())) + keep_alive = self.parser.should_keep_alive() and not self.signal.stopped + self.transport.write(response.output(self.request.version, keep_alive, self.request_timeout)) if not keep_alive: self.transport.close() else: @@ -140,8 +111,8 @@ class HttpProtocol(asyncio.Protocol): except Exception as e: self.bail_out("Writing request failed, connection closed {}".format(e)) - def bail_out(self, error): - log.error(error) + def bail_out(self, message): + log.error(message) self.transport.close() def cleanup(self): @@ -149,39 +120,53 @@ class HttpProtocol(asyncio.Protocol): self.request = None self.url = None self.headers = None - self._total_body_size = 0 + self._total_request_size = 0 -def serve(sanic, host, port, debug=False, on_start=None, on_stop=None): +def serve(host, port, request_handler, on_start=None, on_stop=None, debug=False, request_timeout=60, request_max_size=None): # Create Event Loop loop = async_loop.new_event_loop() asyncio.set_event_loop(loop) loop.set_debug(debug) - if debug: - log.setLevel(logging.DEBUG) - log.debug(sanic.config.LOGO) - - # Serve - log.info('Goin\' Fast @ {}:{}'.format(host, port)) - # Run the on_start function if provided if on_start: - result = on_start(sanic, loop) + result = on_start(loop) if isawaitable(result): loop.run_until_complete(result) - server_coroutine = loop.create_server(lambda: HttpProtocol(loop=loop, sanic=sanic), host, port) - #connection_timeout_coroutine = - server_loop = loop.run_until_complete(server_coroutine) + connections = {} + signal = Signal() + server_coroutine = loop.create_server(lambda: HttpProtocol( + loop=loop, + connections = connections, + signal = signal, + request_handler=request_handler, + request_timeout=request_timeout, + request_max_size=request_max_size, + ), host, port) + http_server = loop.run_until_complete(server_coroutine) + try: loop.run_forever() + except Exception: + pass finally: + log.info("Stop requested, draining connections...") + # Run the on_stop function if provided if on_stop: - result = on_stop(sanic, loop) + result = on_stop(loop) if isawaitable(result): loop.run_until_complete(result) # Wait for event loop to finish and all connections to drain - server_loop.close() - loop.close() \ No newline at end of file + http_server.close() + loop.run_until_complete(http_server.wait_closed()) + + # Complete all tasks on the loop + signal.stopped = True + while connections: + loop.run_until_complete(asyncio.sleep(0.1)) + + loop.close() + log.info("Server Stopped") diff --git a/setup.py b/setup.py index b95ac5fe..cd11c241 100644 --- a/setup.py +++ b/setup.py @@ -11,13 +11,10 @@ setup( author='Channel Cat', author_email='channelcat@gmail.com', description='A microframework based on uvloop and httptools', - #long_description=, packages=['sanic'], - #include_package_data=True, - #zip_safe=False, platforms='any', install_requires=[ - 'uvloop>=0.5.3', + #'uvloop>=0.5.3', 'httptools>=0.0.9', 'ujson>=1.35', ],