glotchimo / apodo

A cancelled Python web server project.
39 stars 1 forks source link

Initial concept server implementation #25

Closed glotchimo closed 5 years ago

glotchimo commented 5 years ago

This PR introduces the first iteration of the core server design implementation. Included in this design are three primary classes: Apodo, Request, and Response.

Apodo

This class controls all high-level operations of the framework. Users interact with this class to register views and run the server. Once the server is running, it catches and routes requests to the proper views.

Request

This class holds view and request information, and is instantiated every time a new request is caught by the main server. The instance is passed to the corresponding view and allows users to work with that request in their views.

Response

This class controls the response to the client. It is not modifiable yet by the user, and only accepts whatever body data is generated and returned by the view. This is going to be amended before this PR is marked as ready to review.

glotchimo commented 5 years ago

In benchmarking on a DigitalOcean 2GB/2CPU droplet, this implementation was able to achieve 250,000 requests per second at 300 threads each issuing 1000 calls. The benchmarking example server and testing script are as follows:

server.py

from apodo import Apodo, Request

server = Apodo("Test Server")

@server.view("/")
async def hello(request: Request):
    """ Returns a basic 'Hello world!' response. """
    return "Hello world!"

if __name__ == "__main__":
    server.serve()

test.py

from time import time
from multiprocessing import Pool
from urllib.request import urlopen

def do(calls: int, port: str):
    """ Makes the given number of calls on the given port at /. """
    for _ in range(calls):
        urlopen(f"http://127.0.0.1:{port}")

if __name__ == "__main__":
    port = str(input("Port to call: "))
    threads = int(input("Number of threads to spawn: "))
    calls = int(input("Number of calls to make: "))

    start = time()
    with Pool(threads):
        do(calls, port)
    end = time()

    total = threads * calls
    duration = end - start
    rps = total / duration

    print(f"{total} calls made in {duration} seconds,")
    print(f"resulting in an average of {rps} requests per second.")
glotchimo commented 5 years ago

This PR also propagates the rename from Gato to Apodo throughout the source code.

glotchimo commented 5 years ago

e39fc62 implements handling for user-modified Response objects, which, in application, would look like this:

from apodo import Apodo, Request, Response

server = Apodo("Test Server")

@server.view("/modify")
async def modify(request: Request):
    """ Echoes the request body and modifies response headers. """
    body = request.body
    headers = {"X-Custom-Header": "Hello world!"}

    response = Response(request.writer, body=body, headers=headers)

    return response