Unverified Commit eb6ffe35 authored by Burak's avatar Burak Committed by GitHub
Browse files

Lambda support for Python servers (#1807)

* Allow running Python servers with Lambda handler

* Fix doctests

* Add `Running servers on AWS Lambda` section to docs

* Remove empty spaces from `README` to make linter happy

* Rename `run_lambda_server` to `run_lambda_handler`

* Add more comments to example `Dockerfile`

* Fix capitalization of logs

* Preserve original formatting of `PythonApplicationGenerator.kt`
parent b547045a
Loading
Loading
Loading
Loading
+32 −28
Original line number Diff line number Diff line
@@ -92,7 +92,6 @@ class PythonApplicationGenerator(
        renderAppDefault(writer)
        renderAppClone(writer)
        renderPyAppTrait(writer)
        renderAppImpl(writer)
        renderPyMethods(writer)
    }

@@ -149,17 +148,35 @@ class PythonApplicationGenerator(
        )
    }

    private fun renderAppImpl(writer: RustWriter) {
    private fun renderPyAppTrait(writer: RustWriter) {
        writer.rustBlockTemplate(
            """
            impl App
            impl #{SmithyPython}::PyApp for App
            """,
            *codegenScope,
        ) {
            rustTemplate(
                """
                fn workers(&self) -> &#{parking_lot}::Mutex<Vec<#{pyo3}::PyObject>> {
                    &self.workers
                }
                fn context(&self) -> &Option<#{pyo3}::PyObject> {
                    &self.context
                }
                fn handlers(&mut self) -> &mut #{HashMap}<String, #{SmithyPython}::PyHandler> {
                    &mut self.handlers
                }
                fn middlewares(&mut self) -> &mut #{SmithyPython}::PyMiddlewares {
                    &mut self.middlewares
                }
                """,
                *codegenScope,
            )

            rustBlockTemplate(
                """
                /// Dynamically codegenerate the routes, allowing to build the Smithy [#{SmithyServer}::routing::Router].
                pub fn build_router(&mut self, event_loop: &#{pyo3}::PyAny) -> #{pyo3}::PyResult<#{SmithyServer}::routing::Router>
                // Dynamically codegenerate the routes, allowing to build the Smithy [#{SmithyServer}::routing::Router].
                fn build_router(&mut self, event_loop: &#{pyo3}::PyAny) -> #{pyo3}::PyResult<#{SmithyServer}::routing::Router>
                """,
                *codegenScope,
            ) {
@@ -203,28 +220,6 @@ class PythonApplicationGenerator(
        }
    }

    private fun renderPyAppTrait(writer: RustWriter) {
        writer.rustTemplate(
            """
            impl #{SmithyPython}::PyApp for App {
                fn workers(&self) -> &#{parking_lot}::Mutex<Vec<#{pyo3}::PyObject>> {
                    &self.workers
                }
                fn context(&self) -> &Option<#{pyo3}::PyObject> {
                    &self.context
                }
                fn handlers(&mut self) -> &mut #{HashMap}<String, #{SmithyPython}::PyHandler> {
                    &mut self.handlers
                }
                fn middlewares(&mut self) -> &mut #{SmithyPython}::PyMiddlewares {
                    &mut self.middlewares
                }
            }
            """,
            *codegenScope,
        )
    }

    private fun renderPyMethods(writer: RustWriter) {
        writer.rustBlockTemplate(
            """
@@ -264,6 +259,15 @@ class PythonApplicationGenerator(
                    use #{SmithyPython}::PyApp;
                    self.run_server(py, address, port, backlog, workers)
                }
                /// Lambda entrypoint: start the server on Lambda.
                ##[pyo3(text_signature = "(${'$'}self)")]
                pub fn run_lambda(
                    &mut self,
                    py: #{pyo3}::Python,
                ) -> #{pyo3}::PyResult<()> {
                    use #{SmithyPython}::PyApp;
                    self.run_lambda_handler(py)
                }
                /// Build the router and start a single worker.
                ##[pyo3(text_signature = "(${'$'}self, socket, worker_number)")]
                pub fn start_worker(
@@ -274,7 +278,7 @@ class PythonApplicationGenerator(
                ) -> pyo3::PyResult<()> {
                    use #{SmithyPython}::PyApp;
                    let event_loop = self.configure_python_event_loop(py)?;
                    let router = self.build_router(event_loop)?;
                    let router = self.build_and_configure_router(py, event_loop)?;
                    self.start_hyper_worker(py, socket, event_loop, router, worker_number)
                }
                """,
+1 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ bytes = "1.2"
futures = "0.3"
http = "0.2"
hyper = { version = "0.14.20", features = ["server", "http1", "http2", "tcp", "stream"] }
lambda_http = "0.6.0"
num_cpus = "1.13.1"
parking_lot = "0.12.1"
pin-project-lite = "0.2"
+67 −0
Original line number Diff line number Diff line
@@ -2,6 +2,73 @@

Server libraries for smithy-rs generated servers, targeting pure Python business logic.

## Running servers on AWS Lambda

`aws-smithy-http-server-python` supports running your services on [AWS Lambda](https://aws.amazon.com/lambda/).

You need to use `run_lambda` method instead of `run` method to start
the [custom runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html)
instead of the [Hyper](https://hyper.rs/) HTTP server.

In your `app.py`:

```diff
from libpokemon_service_server_sdk import App
from libpokemon_service_server_sdk.error import ResourceNotFoundException

# ...

# Get the number of requests served by this server.
@app.get_server_statistics
def get_server_statistics(
    _: GetServerStatisticsInput, context: Context
) -> GetServerStatisticsOutput:
    calls_count = context.get_calls_count()
    logging.debug("The service handled %d requests", calls_count)
    return GetServerStatisticsOutput(calls_count=calls_count)

# ...

-app.run()
+app.run_lambda()
```

`aws-smithy-http-server-python` comes with a
[custom runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html)
so you should run your service without any provided runtimes.
You can achieve that with a `Dockerfile` similar to this:

```dockerfile
# You can use any image that has your desired Python version
FROM public.ecr.aws/lambda/python:3.8-x86_64

# Copy your application code to `LAMBDA_TASK_ROOT`
COPY app.py ${LAMBDA_TASK_ROOT}
# When you build your Server SDK for your service you get a shared library
# that is importable in Python. You need to copy that shared library to same folder
# with your application code, so it can be imported by your application.
# Note that you need to build your library for Linux,
# if you are on a different platform you can consult to
# https://pyo3.rs/latest/building_and_distribution.html#cross-compiling
# for cross compiling.
COPY lib_pokemon_service_server_sdk.so ${LAMBDA_TASK_ROOT}

# You can install your application's dependencies using file `requirements.txt`
# from your project folder, if you have any.
# COPY requirements.txt  .
# RUN  pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"

# Create a symlink for your application's entrypoint,
# so we can use `/app.py` to refer it
RUN ln -s ${LAMBDA_TASK_ROOT}/app.py /app.py

# By default `public.ecr.aws/lambda/python` images comes with Python runtime,
# we need to override `ENTRYPOINT` and `CMD` to not call that runtime and
# instead run directly your service and it will start our custom runtime.
ENTRYPOINT [ "/var/lang/bin/python3.8" ]
CMD [ "/app.py" ]
```

<!-- anchor_start:footer -->
This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/awslabs/smithy-rs) code generator. In most cases, it should not be used directly.
<!-- anchor_end:footer -->
+45 −14
Original line number Diff line number Diff line
@@ -6,7 +6,10 @@

use std::{collections::HashMap, ops::Deref, process, thread};

use aws_smithy_http_server::{routing::Router, AddExtensionLayer};
use aws_smithy_http_server::{
    routing::{LambdaHandler, Router},
    AddExtensionLayer,
};
use parking_lot::Mutex;
use pyo3::{prelude::*, types::IntoPyDict};
use signal_hook::{consts::*, iterator::Signals};
@@ -63,6 +66,9 @@ pub trait PyApp: Clone + pyo3::IntoPy<PyObject> {

    fn middlewares(&mut self) -> &mut PyMiddlewares;

    /// Build the app's `Router` using given `event_loop`.
    fn build_router(&mut self, event_loop: &pyo3::PyAny) -> pyo3::PyResult<Router>;

    /// 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.
@@ -212,9 +218,6 @@ event_loop.add_signal_handler(signal.SIGINT,
        router: Router,
        worker_number: isize,
    ) -> PyResult<()> {
        // Create the `PyState` object from the Python context object.
        let context = self.context().clone().unwrap_or_else(|| py.None());
        // let state = PyState::new(context);
        // Clone the socket.
        let borrow = socket.try_borrow_mut()?;
        let held_socket: &PySocket = &*borrow;
@@ -231,19 +234,14 @@ event_loop.add_signal_handler(signal.SIGINT,
                .thread_name(format!("smithy-rs-tokio[{worker_number}]"))
                .build()
                .expect("Unable to start a new tokio runtime for this process");
            // Register operations into a Router, add middleware and start the `hyper` server,
            // all inside a [tokio] blocking function.
            rt.block_on(async move {
                tracing::debug!("Add middlewares to Rust Python router");
                let app =
                    router.layer(ServiceBuilder::new().layer(AddExtensionLayer::new(context)));
                let server = hyper::Server::from_tcp(
                    raw_socket
                        .try_into()
                        .expect("Unable to convert socket2::Socket into std::net::TcpListener"),
                )
                .expect("Unable to create hyper server from shared socket")
                .serve(app.into_make_service());
                .serve(router.into_make_service());

                tracing::debug!("Started hyper server from shared socket");
                // Run forever-ish...
@@ -374,15 +372,12 @@ event_loop.add_signal_handler(signal.SIGINT,
    ///     #[derive(Debug, Clone)]
    ///     pub struct App {};
    ///
    ///     impl App {
    ///         pub fn build_router(&mut self, event_loop: &PyAny) -> PyResult<aws_smithy_http_server::routing::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!() }
    ///         fn middlewares(&mut self) -> &mut PyMiddlewares { todo!() }
    ///         fn build_router(&mut self, event_loop: &PyAny) -> PyResult<aws_smithy_http_server::routing::Router> { todo!() }
    ///     }
    ///
    ///     #[pymethods]
@@ -457,4 +452,40 @@ event_loop.add_signal_handler(signal.SIGINT,
        self.block_on_rust_signals();
        Ok(())
    }

    /// Lambda main entrypoint: start the handler on Lambda.
    ///
    /// Unlike the `run_server`, `run_lambda_handler` does not spawns other processes,
    /// it starts the Lambda handler on the current process.
    fn run_lambda_handler(&mut self, py: Python) -> PyResult<()> {
        let event_loop = self.configure_python_event_loop(py)?;
        let app = self.build_and_configure_router(py, event_loop)?;
        let rt = runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .expect("unable to start a new tokio runtime for this process");
        rt.block_on(async move {
            let handler = LambdaHandler::new(app);
            let lambda = lambda_http::run(handler);
            tracing::debug!("starting lambda handler");
            if let Err(err) = lambda.await {
                tracing::error!(error = %err, "unable to start lambda handler");
            }
        });
        Ok(())
    }

    // Builds the router and adds necessary layers to it.
    fn build_and_configure_router(
        &mut self,
        py: Python,
        event_loop: &pyo3::PyAny,
    ) -> pyo3::PyResult<Router> {
        let app = self.build_router(event_loop)?;
        // Create the `PyState` object from the Python context object.
        let context = self.context().clone().unwrap_or_else(|| py.None());
        tracing::debug!("add middlewares to rust python router");
        let app = app.layer(ServiceBuilder::new().layer(AddExtensionLayer::new(context)));
        Ok(app)
    }
}