|
| 1 | +--- |
| 2 | +id: async-migrate |
| 3 | +title: How we migrate our framework into async/await |
| 4 | +author: Wai Pai Lee |
| 5 | +author_title: Co-author of Obsidian |
| 6 | +author_url: https://github.com/plwai |
| 7 | +author_image_url: https://avatars2.githubusercontent.com/u/9108726?s=460&v=4 |
| 8 | +tags: [obsidian, log] |
| 9 | +--- |
| 10 | + |
| 11 | +Firstly, congratulation on Rust lang achieving stable async/await syntax! As of the release, async/await is becoming the preferred way to do asynchronous programming instead of using Futures in Rust lang. |
| 12 | + |
| 13 | +In Obsidian Web Framework, we do the same move just like other libraries which enabling async/await syntax in order to provide a better development experience. |
| 14 | + |
| 15 | +_*Rust 1.40 is used in this article_ |
| 16 | + |
| 17 | +<!--truncate--> |
| 18 | + |
| 19 | +# In case you don't know async/await in Rust |
| 20 | + |
| 21 | +<div align="center"><iframe width="560" height="315" src="https://www.youtube.com/embed/lJ3NC-R3gSI" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> |
| 22 | + |
| 23 | +# Why async/await? |
| 24 | + |
| 25 | +__Code complexity__ - You may design the code in the synchronous way. |
| 26 | + |
| 27 | +__Readability__ - Avoid `and_then` and `then` chaining which making code hard to read. |
| 28 | + |
| 29 | +__Lifetime__ - Lifetime is following synchronous flow within `async` function. |
| 30 | + |
| 31 | +In Obsidian, we treat the developer's development experience as the first priority goal. Thus, migration to async/await enabled structure is definitely needed. |
| 32 | + |
| 33 | +# Changing futures to async/await |
| 34 | + |
| 35 | +The process is pretty simple for most of the cases. Basically, we just add async to the function and remove `then` ,`and_then` and the dangerous `wait` to `await`. From the example below, it shows that the code is much cleaner and readable after using async/await. |
| 36 | + |
| 37 | +from |
| 38 | + |
| 39 | +```rust |
| 40 | +pub fn file(file_path: &str) -> ResponseResult { |
| 41 | + tokio_fs::file::File::open(file_path.to_string()) |
| 42 | + .and_then(|file| { |
| 43 | + let buf: Vec<u8> = Vec::new(); |
| 44 | + tokio_io::io::read_to_end(file, buf) |
| 45 | + .and_then(|item| { |
| 46 | + Ok(Response::builder() |
| 47 | + .status(StatusCode::OK) |
| 48 | + .body(item.1.into()) |
| 49 | + .unwrap()) |
| 50 | + }) |
| 51 | + .or_else(|_| { |
| 52 | + Ok(Response::builder() |
| 53 | + .status(StatusCode::INTERNAL_SERVER_ERROR) |
| 54 | + .body(Body::empty()) |
| 55 | + .unwrap()) |
| 56 | + }) |
| 57 | + }) |
| 58 | + .or_else(|err| { |
| 59 | + dbg!(&err); |
| 60 | + Ok(Response::builder() |
| 61 | + .status(StatusCode::NOT_FOUND) |
| 62 | + .body(NOTFOUND.into()) |
| 63 | + .unwrap()) |
| 64 | + }) |
| 65 | + .wait() |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +to |
| 70 | + |
| 71 | +```rust |
| 72 | +pub async fn file(file_path: &str) -> ResponseResult { |
| 73 | + match fs::read(file_path.to_string()).await { |
| 74 | + Ok(buf) => { |
| 75 | + Ok(Response::builder() |
| 76 | + .status(StatusCode::OK) |
| 77 | + .body(buf.into()) |
| 78 | + .unwrap()) |
| 79 | + }, |
| 80 | + Err(err) => { |
| 81 | + Ok(Response::builder() |
| 82 | + .status(StatusCode::NOT_FOUND) |
| 83 | + .body(NOTFOUND.into()) |
| 84 | + .unwrap()) |
| 85 | + }, |
| 86 | + } |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +# Using async/await in trait and closure |
| 91 | + |
| 92 | +In current Rust version, the `async` syntax does not support trait function and closure yet. It will be supported in the future. So, we need to do it another way around which is like this. |
| 93 | + |
| 94 | +```rust |
| 95 | +// trait |
| 96 | +pub trait Middleware: Send + Sync + 'static { |
| 97 | + fn handle<'a>( |
| 98 | + &'a self, |
| 99 | + context: Context, |
| 100 | + ep_executor: EndpointExecutor<'a>, |
| 101 | + ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Response<Body>> + Send + 'a>>; |
| 102 | +} |
| 103 | + |
| 104 | +impl Middleware for Logger { |
| 105 | + fn handle<'a>( |
| 106 | + &'a self, |
| 107 | + context: Context, |
| 108 | + ep_executor: EndpointExecutor<'a>, |
| 109 | + ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Response<Body>> + Send + 'a>> { |
| 110 | + let run = move |_self: &Logger| async { |
| 111 | + println!( |
| 112 | + "{} {} \n{}", |
| 113 | + context.method(), |
| 114 | + context.uri(), |
| 115 | + context.headers().get("host").unwrap().to_str().unwrap() |
| 116 | + ); |
| 117 | + |
| 118 | + ep_executor.next(context).await |
| 119 | + }; |
| 120 | + |
| 121 | + Box::pin(run(self)) |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +// closure |
| 126 | +fn main() { |
| 127 | + // A closure which return async block instead of using async closure |
| 128 | + let closure = || async { |
| 129 | + something().await |
| 130 | + } |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +It seems complicated for a trait but luckily Rust community create a crate to simplify this which is called as `async-trait`. By using this crate, we are able to use traits with asyncsyntax! |
| 135 | + |
| 136 | +```rust |
| 137 | +#[async_trait] |
| 138 | +pub trait Middleware: Send + Sync + 'static { |
| 139 | + async fn handle<'a>( |
| 140 | + &'a self, |
| 141 | + context: Context, |
| 142 | + ep_executor: EndpointExecutor<'a>, |
| 143 | + ) -> Response<Body>; |
| 144 | +} |
| 145 | + |
| 146 | +#[async_trait] |
| 147 | +impl Middleware for Logger { |
| 148 | + async fn handle<'a>( |
| 149 | + &'a self, |
| 150 | + context: Context, |
| 151 | + ep_executor: EndpointExecutor<'a>, |
| 152 | + ) -> Response<Body> { |
| 153 | + println!( |
| 154 | + "{} {} \n{}", |
| 155 | + context.method(), |
| 156 | + context.uri(), |
| 157 | + context.headers().get("host").unwrap().to_str().unwrap() |
| 158 | + ); |
| 159 | + |
| 160 | + ep_executor.next(context).await |
| 161 | + } |
| 162 | +} |
| 163 | +``` |
| 164 | + |
| 165 | +You may notice that this trait workaround method will result in a heap allocation per-function-call. So why do we use it? In Obsidian, we aim for delivering a better development experience. We believe that enabling await for traits like `Middleware` ease the development process. Besides that, we also trust the Rust community will enable the use of `async` in trait soon. 😀 |
| 166 | + |
| 167 | +For more information on the trade-off, visit [Asynchronous Programming in Rust](https://rust-lang.github.io/async-book/07_workarounds/06_async_in_traits.html) book. |
| 168 | + |
| 169 | +# How to test an async API? |
| 170 | + |
| 171 | +Sadly, now test function cannot use `async` syntax. Thus, we need to make it synchronous. In our framework, we use `async-std` task blocking feature to make it happen. |
| 172 | + |
| 173 | +```rust |
| 174 | +#[test] |
| 175 | +fn test_form() -> Result<(), ObsidianError> { |
| 176 | + task::block_on(async { |
| 177 | + let params = HashMap::default(); |
| 178 | + let request = Request::new(Body::from("id=1&mode=edit")); |
| 179 | + |
| 180 | + let mut ctx = Context::new(request, params); |
| 181 | + |
| 182 | + let actual_result: FormResult = ctx.form().await?; |
| 183 | + let expected_result = FormResult { |
| 184 | + id: 1, |
| 185 | + mode: "edit".to_string(), |
| 186 | + }; |
| 187 | + |
| 188 | + assert_eq!(actual_result, expected_result); |
| 189 | + Ok(()) |
| 190 | + }) |
| 191 | +} |
| 192 | +``` |
| 193 | + |
| 194 | +# Lifetime issue |
| 195 | + |
| 196 | +Although async/await seems perfect, it can be hard when dealing with `self` or struct outside of async block lifetime. In some cases, the developer might want to call an async implementation within the same struct. In this case, the compiler will not let you go. For instance: |
| 197 | + |
| 198 | +```rust |
| 199 | +let service = make_service_fn(|_| { |
| 200 | + let server_clone = app_server.clone(); |
| 201 | + async { |
| 202 | + Ok::<_, hyper::Error>(service_fn(move |req| { |
| 203 | + let server_clone = server_clone.clone(); |
| 204 | + async move { Ok::<_, hyper::Error>(server_clone.resolve_endpoint(req).await) } |
| 205 | + })) |
| 206 | + } |
| 207 | +}); |
| 208 | +``` |
| 209 | + |
| 210 | +The simplest way will be simply cloning the struct before async block call which is used above. However, the system will have the scalability issue. In Obsidian, the clone method decreased performance by ~25% with the sample of 1 endpoint router and 20 endpoint router. So, we decided to go for another approach by moving out the usage of `self` into synchronous part and move its ownership into the async block for the async process. |
| 211 | + |
| 212 | +```rust |
| 213 | + |
| 214 | +let service = make_service_fn(|_| { |
| 215 | + let server_clone = app_server.clone(); |
| 216 | + async { |
| 217 | + Ok::<_, hyper::Error>(service_fn(move |req| { |
| 218 | + let route_value = server_clone.router.search_route(req.uri().path()); |
| 219 | + AppServer::resolve_endpoint(req, route_value) |
| 220 | + })) |
| 221 | + } |
| 222 | +}); |
| 223 | +``` |
| 224 | + |
| 225 | +# About Obsidian |
| 226 | + |
| 227 | +[Obsidian](https://github.com/obsidian-rs/obsidian) is a web development framework built in Rust which vision to lower down the learning curve of web development in Rust without losing the advantage of it. Currently, Obsidian is under active development and we expected to release v0.2 in this couple of months. |
0 commit comments