Up until we implemented the connection and querying of the database our function's runtime was single-digit milliseconds and the initialization from a cold-start was around 25ms.
With the connection initialization and querying both being in the handler, our cold-start initialization time hasn't changed much, but our runtime ballooned to 82ms.
2:38:36 AM: cold start
2:38:36 AM: handler
2:38:36 AM: aca64e07 Duration: 82.17 ms Memory Usage: 20 MB Init Duration: 39.52 ms
2:38:48 AM: handler
2:38:48 AM: 6ea28ecd Duration: 29.28 ms Memory Usage: 20 MB
2:38:52 AM: handler
2:38:52 AM: c3b6c273 Duration: 22.93 ms Memory Usage: 20 MB
2:39:16 AM: handler
2:39:17 AM: 956d573e Duration: 31.40 ms Memory Usage: 20 MB
2:42:10 AM: handler
2:42:10 AM: 9f562f18 Duration: 30.85 ms Memory Usage: 20 MB
2:42:13 AM: handler
2:42:13 AM: 8cd93d17 Duration: 22.59 ms Memory Usage: 20 MB
2:42:14 AM: handler
2:42:14 AM: fe8b3ccc Duration: 33.81 ms Memory Usage: 20 MB
2:42:16 AM: handler
2:42:16 AM: b2f2de41 Duration: 23.07 ms Memory Usage: 20 MB
The work we're doing in a serverless function splits into two major pieces:
- The work done when initializing: aka the "cold start" work
- The work we do on every request: aka the
handler
Our initialization work doesn't have much to do compared to our pre-sql function, as evidenced by the fact that our previous cold start time and our current cold start times are more or less the same.
Looking at our handler function, we have a couple segments of work happening.
- Retrieve an environment variable
- Set up a connection pool
- Query to the database
- Serialize our result to JSON
Without measuring we can make a couple assumptions about the performance of this work and where it should happen.
Netlify doesn't let us change environment variables while a function is running, so it seems reasonable to only get it once when the function starts up.
The connection pool handles instantiating connections to the database when we need them. On first initialization, it connects to the database once, and if the pool needs more it will spin up more connections.
By placing the initialization of the connection pool in the handler function, we're ensuring that a new connection is established on each invocation of the lambda. If we can instead move this to the cold start, then we should be able to re-use connections from the pool across warm lambda invocations.
Querying the database has to happen in the handler, because we'll be using user input to adjust the query.
Serializing our JSON relies on the SQL query request so it has to be in the handler as well.
So the only large piece of work we need in our handler is the sql query.
Moving work to the initialization phase
To move the connection pool to the initialization phase, we'll use a new data type: OnceCell
. OnceCell
comes from a third party crate called once_cell
.
cargo add -p pokemon-api once_cell
Cells are data structures that exist in the standard library under the std::cell
module path. OnceCell
is an extension to this set of data structures that can only be set once, hence the name: "once" cell.
We can read the value as many times as we need to, later in our program.
We need to bring OnceCell
, MySql
, and Pool
into scope.
use once_cell::sync::OnceCell;
use sqlx::{mysql::MySqlPoolOptions, MySql, Pool};
To construct a new OnceCell
, we can use OnceCell::new()
which returns a new OnceCell
struct.
OnceCell
takes a type argument that indicates what kind of value we're going to store in it. We're going to store an instantiated MySql connection pool, so the type of POOL
will be OnceCell<Pool<MySql>>
.
static POOL: OnceCell<Pool<MySql>> = OnceCell::new();
Which brings us to static
. The static
declaration is at the root of our file, it's not inside of any function like our let
declarations are.
static
basically means: make one of these and only one of these. All references will refer to this specific instance and there will never be a second.
We can continue in our main
function, moving our database_url
and pool
initialization work from the match expression into main
.
To actually initialize the POOL
item, we can use get_or_init
, which will call init
if POOL
hasn't been initialized yet. get_or_init
takes a closure, which we return our initialized pool
from. This moves pool
into POOL
and enables us to access it later in our handler.
async fn main() -> Result<(), Error> {
println!("cold start");
let database_url = env::var("DATABASE_URL")?;
let pool = MySqlPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await?;
POOL.get_or_init(|| pool);
let processor = handler_fn(handler);
lambda_runtime::run(processor).await?;
Ok(())
}
In our handler, we can use .get()
to get access to the connection pool. .get()
could fail, although it won't for us, so we can .unwrap()
. This will panic if the POOL
isn't set, but we always set it in the cold start phase, so if it doesn't exist something has gone very wrong.
.fetch_one(POOL.get().unwrap())
This leaves us with quite a difference in cold start vs handler times. The cold start is over 70 ms, while the warm execution time of our actual data fetch is around 5-10ms.
3:11:20 AM: cold start
3:11:20 AM: handler
3:11:20 AM: b7b1dfc6 Duration: 9.09 ms Memory Usage: 20 MB Init Duration: 73.42 ms
3:11:32 AM: handler
3:11:32 AM: 2683037b Duration: 5.84 ms Memory Usage: 20 MB
3:11:35 AM: handler
3:11:35 AM: faec7a85 Duration: 6.10 ms Memory Usage: 20 MB
3:11:38 AM: handler
3:11:38 AM: 2b23546c Duration: 5.54 ms Memory Usage: 20 MB
3:13:08 AM: handler
3:13:08 AM: 9b1be575 Duration: 6.59 ms Memory Usage: 20 MB
3:13:11 AM: handler
3:13:11 AM: 384cf547 Duration: 4.56 ms Memory Usage: 20 MB
3:13:14 AM: handler
3:13:14 AM: 27cb637d Duration: 4.61 ms Memory Usage: 20 MB
3:13:17 AM: handler
3:13:17 AM: 9e7353f9 Duration: 4.65 ms Memory Usage: 20 MB
We also need to update our test function to connect to the database. This is the same logic we had before in our main function, with unwrap
instead of ?
.
async fn handler_handles() {
let database_url =
env::var("DATABASE_URL").unwrap();
let pool = MySqlPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await
.unwrap();
POOL.get_or_init(|| pool);
All tests should now pass.