pub trait PyApp: Clone + IntoPy<PyObject> {
    fn workers(&self) -> &Mutex<Vec<PyObject>>;
    fn context(&self) -> &Option<PyObject>;
    fn handlers(&mut self) -> &mut HashMap<String, PyHandler>;

    fn graceful_termination(&self, workers: &Mutex<Vec<PyObject>>) -> ! { ... }
    fn immediate_termination(&self, workers: &Mutex<Vec<PyObject>>) -> ! { ... }
    fn block_on_rust_signals(&self) { ... }
    fn register_python_signals(
        &self,
        py: Python<'_>,
        event_loop: PyObject
    ) -> PyResult<()> { ... } fn start_hyper_worker(
        &mut self,
        py: Python<'_>,
        socket: &PyCell<PySocket>,
        event_loop: &PyAny,
        router: Router,
        worker_number: isize
    ) -> PyResult<()> { ... } fn register_operation(
        &mut self,
        py: Python<'_>,
        name: &str,
        func: PyObject
    ) -> PyResult<()> { ... } fn configure_python_event_loop<'py>(
        &self,
        py: Python<'py>
    ) -> PyResult<&'py PyAny> { ... } fn run_server(
        &mut self,
        py: Python<'_>,
        address: Option<String>,
        port: Option<i32>,
        backlog: Option<i32>,
        workers: Option<usize>
    ) -> PyResult<()> { ... } }
Expand description

Trait defining a Python application.

A Python application requires handling of multiple processes, signals and allows to register Python function that will be executed as business logic by the code generated Rust handlers. To properly function, the application requires some state:

  • workers: the list of child Python worker processes, protected by a Mutex.
  • context: the optional Python object that should be passed inside the Rust state struct.
  • handlers: the mapping between an operation name and its PyHandler representation.

Since the Python application is spawning multiple workers, it also requires signal handling to allow the gracefull termination of multiple Hyper servers. The main Rust process is registering signal and using them to understand when it it time to loop through all the active workers and terminate them. Workers registers their own signal handlers and attaches them to the Python event loop, ensuring all coroutines are cancelled before terminating a worker.

This trait will be implemented by the code generated by the PythonApplicationGenerator Kotlin class.

Required Methods

List of active Python workers registered with this application.

Optional Python context object that will be passed as part of the Rust state.

Mapping between operation names and their PyHandler representation.

Provided Methods

Handle the graceful termination of Python workers by looping through all the active workers and calling terminate() on them. If termination fails, this method will try to kill() any failed worker.

Handler the immediate termination of Python workers by looping through all the active workers and calling kill() on them.

Register and handler signals of the main Rust thread. Signals not registered in this method are ignored.

Signals supported:

  • SIGTERM|SIGQUIT - graceful termination of all workers.
  • SIGINT - immediate termination of all workers.

Other signals are NOOP.

Register and handle termination of all the tasks on the Python asynchronous event loop. We only register SIGQUIT and SIGINT since the main signal handling is done by Rust.

Start a single worker with its own Tokio and Python async runtime and provided shared socket.

Python asynchronous loop needs to be started and handled during the lifetime of the process and it is passed to this method by the caller, which can use configure_python_event_loop to properly setup it up.

We retrieve the Python context object, if setup by the user calling PyApp::context method, generate the state structure and build the aws_smithy_http_server::Router, filling it with the functions generated by PythonServerOperationHandlerGenerator.kt. At last we get a cloned reference to the underlying socket2::Socket.

Now that all the setup is done, we can start the two runtimes and run the hyper server. We spawn a thread with a new tokio::runtime, setup the middlewares and finally block the thread on Hyper serve() method. The main process continues and at the end it is blocked on Python loop.run_forever().

Register a Python function to be executed inside the Smithy Rust handler.

There are some information needed to execute the Python code from a Rust handler, such has if the registered function needs to be awaited (if it is a coroutine) and the number of arguments available, which tells us if the handler wants the state to be passed or not.

Configure the Python asyncio event loop.

First of all we install uvloop as the main Python event loop. Thanks to libuv, uvloop performs ~20% better than Python standard event loop in most benchmarks, while being 100% compatible. If uvloop is not available as a dependency, we just fall back to the standard Python event loop.

Main entrypoint: start the server on multiple workers.

The multiprocessing server is achieved using the ability of a Python interpreter to clone and start itself as a new process. The shared sockets is created and Using the multiprocessing::Process module, multiple workers with the method self.start_worker() as target are started.

NOTE: this method ends up calling self.start_worker from the Python context, forcing the struct implementing this trait to also implement a start_worker method. This is done to ensure the Python event loop is started in the right child process space before being passed to start_hyper_worker.

PythonApplicationGenerator.kt generates the start_worker method:

    use std::collections::HashMap;
    use pyo3::prelude::*;
    use aws_smithy_http_server_python::{PyApp, PyHandler};
    use parking_lot::Mutex;

    #[pyclass]
    #[derive(Debug, Clone)]
    pub struct App {};

    impl App {
        pub fn build_router(&mut self, event_loop: &PyAny) -> PyResult<aws_smithy_http_server::Router> { todo!() }
    }

    impl PyApp for App {
        fn workers(&self) -> &Mutex<Vec<PyObject>> { todo!() }
        fn context(&self) -> &Option<PyObject> { todo!() }
        fn handlers(&mut self) -> &mut HashMap<String, PyHandler> { todo!() }
    }

    #[pymethods]
    impl App {
    #[pyo3(text_signature = "($self, socket, worker_number)")]
        pub fn start_worker(
            &mut self,
            py: pyo3::Python,
            socket: &pyo3::PyCell<aws_smithy_http_server_python::PySocket>,
            worker_number: isize,
        ) -> pyo3::PyResult<()> {
            let event_loop = self.configure_python_event_loop(py)?;
            let router = self.build_router(event_loop)?;
            self.start_hyper_worker(py, socket, event_loop, router, worker_number)
        }
    }

Implementors