{"id":6975,"date":"2020-01-04T20:27:25","date_gmt":"2020-01-05T04:27:25","guid":{"rendered":"https:\/\/www.ultrasaurus.com\/?p=6975"},"modified":"2020-01-05T20:24:51","modified_gmt":"2020-01-06T04:24:51","slug":"rust-on-heroku-with-async-await-and-tokio","status":"publish","type":"post","link":"https:\/\/www.ultrasaurus.com\/2020\/01\/rust-on-heroku-with-async-await-and-tokio\/","title":{"rendered":"rust on heroku with async\/await and tokio"},"content":{"rendered":"

In an effort to understand the new Rust async\/await syntax, I made a super-simple app that simply responds to all HTTP requests with Hello!<\/strong> and deployed on Heroku<\/a>.<\/p>\n

Update: If you just want to create a webservice in Rust and deploy on Heroku, I recommend next blog post: rust on heroku with hyper http<\/a>. This blog post focuses on the details of how the underlying request and response is handled with async\/await, on stable Rust since 11\/2019<\/a>.<\/p>\n

The full source code and README instructions can be found on github.com\/ultrasaurus\/hello-heroku-rust, tokio-only branch<\/a><\/p>\n

Rust “hello world” app<\/h2>\n

Make a new project with cargo<\/p>\n

cargo new hello_rust --bin\ncd hello_rust\ngit init\ngit add .\ngit commit -m \u201ccargo new hello_rust \u2014bin\u201d\n\ncargo run\n<\/code><\/pre>\n

output:<\/p>\n

   Compiling hello_rust v0.1.0 (\/Users\/sallen\/src\/rust\/hello_rust)\n    Finished dev [unoptimized + debuginfo] target(s) in 1.47s\n     Running `target\/debug\/hello_rust`\nHello, world!\n<\/code><\/pre>\n

Heroku setup<\/h2>\n

Rust isn’t officially supported by Heroku yet, but there are lots of “buildpacks” which help to deploy a Rust app. I picked emk\/heroku-buildpack-rust<\/a> — most stars, most forks & recently updated!<\/p>\n

We need the heroku CLI<\/a>. I already had it and just did heroku update<\/code> to sync to latest version (7.35.1<\/code>). Then to set up the app on heroku:<\/p>\n

heroku create --buildpack emk\/rust\n<\/code><\/pre>\n

output provides a unique hostname by default:<\/p>\n

Creating app... done, \u2b22 peaceful-gorge-05620\nSetting buildpack to emk\/rust... done\nhttps:\/\/peaceful-gorge-05620.herokuapp.com\/ | https:\/\/git.heroku.com\/peaceful-gorge-05620.git\n<\/code><\/pre>\n

We need a Procfile so heroku knows our entrypoint<\/p>\n

echo \"web: .\/target\/release\/hello_rust\" >> Procfile\n<\/code><\/pre>\n

Write the app<\/h2>\n

Add crate dependencies to Cargo.toml<\/code> and add code to main.rs<\/code> (and other files as with any Rust app). The emk\/rust buildpack<\/em> takes care of building everything as part of the heroku deploy.<\/p>\n

The following lines (in Cargo.toml<\/code>) will add all of tokio features:<\/p>\n

[dependencies]\ntokio = { version = \"0.2\", features = [\"full\"] }\n<\/code><\/pre>\n

I’d rather specify only what’s needed, but ran into something I couldn’t debug myself (issue#2050<\/a>)<\/p>\n

The core of the app accepts the sockets connections, but doesn’t read\/write:<\/p>\n

use std::env;\nuse tokio::net::TcpListener;\n\n#[tokio::main]\nasync fn main() {\n    \/\/ Get the port number to listen on (required for heroku deployment).\n    let port = env::var(\"PORT\").unwrap_or_else(|_| \"1234\".to_string());\n\n    let addr = format!(\"0.0.0.0:{}\", port);\n    let mut listener = TcpListener::bind(addr).await.unwrap();\n\n    loop {\n        println!(\"listening on port {}...\", port);\n        let result = listener.accept().await;\n        match result {\n            Err(e) => println!(\"listen.accept() failed, err: {:?}\", e),\n            Ok(listen) => {\n                let (socket, addr) = listen;\n                println!(\"socket connection accepted, {}\", addr);\n                println!(\"not doing anything yet\");\n            }\n        }\n    }\n}\n<\/code><\/pre>\n

Deploy on heroku<\/h2>\n

The above code will build and deploy, by simply pushing the code to heroku:<\/p>\n

heroku push origin master\n<\/code><\/pre>\n

We can see what it is doing with heroku logs --tail<\/code>:<\/p>\n

Here’s where it starts the build and then kills the old app:<\/p>\n

2020-01-05T03:45:31.000000+00:00 app[api]: Build started by user ...\n2020-01-05T03:45:50.450898+00:00 heroku[web.1]: Restarting\n2020-01-05T03:45:50.454311+00:00 heroku[web.1]: State changed from up to starting\n2020-01-05T03:45:50.244579+00:00 app[api]: Deploy 399e1c85 by user ...\n2020-01-05T03:45:50.244579+00:00 app[api]: Release v24 created by user ...\n2020-01-05T03:45:50.701533+00:00 heroku[web.1]: Starting process with command `.\/target\/release\/hello_rust`\n2020-01-05T03:45:51.741040+00:00 heroku[web.1]: Stopping all processes with SIGTERM\n2020-01-05T03:45:51.819864+00:00 heroku[web.1]: Process exited with status 143\n<\/code><\/pre>\n

Oddly, it seems to start the app before “State changed from starting to up” but it will fail if we’re not listening on the right port, so maybe that is as expected:<\/p>\n

2020-01-05T03:45:52.343368+00:00 app[web.1]: listening on port 49517...\n2020-01-05T03:45:53.322238+00:00 heroku[web.1]: State changed from starting to up\n2020-01-05T03:45:53.303486+00:00 app[web.1]: socket connection accepted, 10.171.202.59:17201\n2020-01-05T03:45:53.303545+00:00 app[web.1]: not doing anything yet\n2020-01-05T03:45:53.303619+00:00 app[web.1]: listening on port 49517...\n2020-01-05T03:45:53.313259+00:00 app[web.1]: socket connection accepted, 172.17.146.217:43686\n2020-01-05T03:45:53.313285+00:00 app[web.1]: not doing anything yet\n2020-01-05T03:45:53.313370+00:00 app[web.1]: listening on port 49517...\n2020-01-05T03:46:28.000000+00:00 app[api]: Build succeeded\n2020-01-05T03:46:48.251168+00:00 heroku[router]: at=error code=H13 desc=\"Connection closed without response\" method=GET path=\"\/\" host=peaceful-gorge-05620.herokuapp.com request_id=a0d630d9-790a-47db-87af-67e680b27907 fwd=\"69.181.194.59\" dyno=web.1 connect=1ms service=1ms status=503 bytes=0 protocol=https\n<\/code><\/pre>\n

So, the first socket connection above is some internal heroku checker, then when I attempt to go to the app URL in the browser, it fails (as expected).<\/p>\n

Async read and write<\/h2>\n

I tried to keep the code clear with as little magic<\/em> as possible. It’s a bit verbose (without even handling HTTP in any general way), but I found it helpful to see the details of read and write.<\/p>\n

Note that adding use tokio::prelude::*;<\/code> allows calling of read_line<\/code> (defined in tokio::io::AsyncBufReadExt<\/code>) and write_all<\/code> (defined in tokio::io::AsyncWriteExt<\/code>).
\nThe additional code reads the bytes from the socket line by line until we get the the end of the HTTP Request (signalled by a blank line). So we look for two CLRFs (one at the end of the last header line and one for the blank line).<\/p>\n

tokio::spawn(async move<\/code> makes it so sure we can read\/write from one socket while also listening for additional connections. tokio::spawn<\/code> will allow the program execution to continue, while concurrently allowing our async function process_socket<\/code> to read and write from the socket. Because we added #[tokio::main]<\/code> above our async fn main<\/code> entry point, tokio will set up an executor which will wait for all of our spawned tasks to complete before exiting.<\/p>\n

use std::env;\nuse tokio::net::TcpListener;\nuse tokio::prelude::*;\n\n#[tokio::main]\nasync fn main() {\n    \/\/ Get the port number to listen on (required for heroku deployment).\n    let port = env::var(\"PORT\").unwrap_or_else(|_| \"1234\".to_string());\n\n    let addr = format!(\"0.0.0.0:{}\", port);\n    let mut listener = TcpListener::bind(addr).await.unwrap();\n\n    loop {\n        println!(\"listening on port {}...\", port);\n        let result = listener.accept().await;\n        match result {\n            Err(e) => println!(\"listen.accept() failed, err: {:?}\", e),\n            Ok(listen) => {\n                let (socket, addr) = listen;\n                println!(\"socket connection accepted, {}\", addr);\n                \/\/ Process each socket concurrently.\n                tokio::spawn(async move {\n                    let mut buffed_socket = tokio::io::BufReader::new(socket);\n                    let mut request = String::new();\n                    let mut result;\n                    loop {\n                        result = buffed_socket.read_line(&mut request).await;\n                        if let Ok(num_bytes) = result {\n                            if num_bytes > 0 && request.len() >= 4 {\n                                let end_chars = &request[request.len() - 4..];\n                                if end_chars == \"\\r\\n\\r\\n\" {\n                                    break;\n                                };\n                            }\n                        }\n                    }\n                    if let Err(e) = result {\n                        println!(\"failed to read from socket, err: {}\", e);\n                        return;\n                    }\n                    let html = \"<h1>Hello!<\/h1>\";\n                    println!(\"request: {}\", request);\n                    let response = format!(\n                        \"HTTP\/1.1 200\\r\\nContent-Length: {}\\r\\n\\r\\n{}\",\n                        html.len(),\n                        html\n                    );\n                    let write_result = buffed_socket.write_all(response.as_bytes()).await;\n                    if let Err(e) = write_result {\n                        println!(\"failed to write, err: {}\", e);\n                    }\n                });\n            }\n        }\n    }\n}\n<\/code><\/pre>\n

Background<\/h2>\n

Here’s my environment info (rustup show<\/code>):<\/p>\n

stable-x86_64-apple-darwin (default)\nrustc 1.39.0 (4560ea788 2019-11-04)\n<\/code><\/pre>\n

Reference docs<\/p>\n