From 272e3912049efdf21c50f1f68a28ac492840dcce Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Tue, 21 May 2024 19:26:15 +0000 Subject: [PATCH 01/36] Add async-chat stub --- exercise-solutions/Cargo.toml | 1 + exercise-solutions/async-chat/Cargo.toml | 8 + exercise-solutions/async-chat/src/client.rs | 46 +++++ exercise-solutions/async-chat/src/main.rs | 13 ++ exercise-solutions/async-chat/src/server.rs | 187 ++++++++++++++++++++ 5 files changed, 255 insertions(+) create mode 100644 exercise-solutions/async-chat/Cargo.toml create mode 100644 exercise-solutions/async-chat/src/client.rs create mode 100644 exercise-solutions/async-chat/src/main.rs create mode 100644 exercise-solutions/async-chat/src/server.rs diff --git a/exercise-solutions/Cargo.toml b/exercise-solutions/Cargo.toml index 83a8c024..44b6dac8 100644 --- a/exercise-solutions/Cargo.toml +++ b/exercise-solutions/Cargo.toml @@ -10,4 +10,5 @@ members = [ "shapes-part-2", "shapes-part-3", "tcp-server-exercises", + "async-chat", ] diff --git a/exercise-solutions/async-chat/Cargo.toml b/exercise-solutions/async-chat/Cargo.toml new file mode 100644 index 00000000..b7fa2c1c --- /dev/null +++ b/exercise-solutions/async-chat/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "async-chat" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures = "0.3" +async-std = "1.6" \ No newline at end of file diff --git a/exercise-solutions/async-chat/src/client.rs b/exercise-solutions/async-chat/src/client.rs new file mode 100644 index 00000000..47e9a668 --- /dev/null +++ b/exercise-solutions/async-chat/src/client.rs @@ -0,0 +1,46 @@ + +use futures::select; +use futures::FutureExt; + +use async_std::{ + io::{stdin, BufReader}, + net::{TcpStream, ToSocketAddrs}, + prelude::*, + task, +}; + +type Result = std::result::Result>; + +pub(crate) fn main() -> Result<()> { + task::block_on(try_main("127.0.0.1:8080")) +} + +async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { + let stream = TcpStream::connect(addr).await?; + let (reader, mut writer) = (&stream, &stream); + let reader = BufReader::new(reader); + let mut lines_from_server = futures::StreamExt::fuse(reader.lines()); + + let stdin = BufReader::new(stdin()); + let mut lines_from_stdin = futures::StreamExt::fuse(stdin.lines()); + loop { + select! { + line = lines_from_server.next().fuse() => match line { + Some(line) => { + let line = line?; + println!("{}", line); + }, + None => break, + }, + line = lines_from_stdin.next().fuse() => match line { + Some(line) => { + let line = line?; + writer.write_all(line.as_bytes()).await?; + writer.write_all(b"\n").await?; + } + None => break, + } + } + } + Ok(()) +} \ No newline at end of file diff --git a/exercise-solutions/async-chat/src/main.rs b/exercise-solutions/async-chat/src/main.rs new file mode 100644 index 00000000..1e30ccd3 --- /dev/null +++ b/exercise-solutions/async-chat/src/main.rs @@ -0,0 +1,13 @@ +mod client; +mod server; + +type Result = std::result::Result>; + +fn main() -> Result<()> { + let mut args = std::env::args(); + match (args.nth(1).as_ref().map(String::as_str), args.next()) { + (Some("client"), None) => client::main(), + (Some("server"), None) => server::main(), + _ => Err("Usage: a-chat [client|server]".into()), + } +} \ No newline at end of file diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs new file mode 100644 index 00000000..0c24cf2d --- /dev/null +++ b/exercise-solutions/async-chat/src/server.rs @@ -0,0 +1,187 @@ +use std::{ + collections::hash_map::{Entry, HashMap}, + sync::Arc, +}; + +use futures::{channel::mpsc, select, FutureExt, SinkExt}; + +use async_std::{ + io::BufReader, + net::{TcpListener, TcpStream, ToSocketAddrs}, + prelude::*, + task, +}; + +type Result = std::result::Result>; +type Sender = mpsc::UnboundedSender; +type Receiver = mpsc::UnboundedReceiver; + +#[derive(Debug)] +enum Void {} + +pub(crate) fn main() -> Result<()> { + task::block_on(accept_loop("127.0.0.1:8080")) +} + +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + let listener = TcpListener::bind(addr).await?; + + let (broker_sender, broker_receiver) = mpsc::unbounded(); + let broker = task::spawn(broker_loop(broker_receiver)); + let mut incoming = listener.incoming(); + while let Some(stream) = incoming.next().await { + let stream = stream?; + println!("Accepting from: {}", stream.peer_addr()?); + spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); + } + drop(broker_sender); + broker.await; + Ok(()) +} + +async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result<()> { + let stream = Arc::new(stream); + let reader = BufReader::new(&*stream); + let mut lines = reader.lines(); + + let name = match lines.next().await { + None => return Err("peer disconnected immediately".into()), + Some(line) => line?, + }; + + println!("user {} connected", name); + + let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded::(); + broker + .send(Event::NewPeer { + name: name.clone(), + stream: Arc::clone(&stream), + shutdown: shutdown_receiver, + }) + .await + .unwrap(); + + while let Some(line) = lines.next().await { + let line = line?; + let (dest, msg) = match line.find(':') { + None => continue, + Some(idx) => (&line[..idx], line[idx + 1..].trim()), + }; + let dest: Vec = dest + .split(',') + .map(|name| name.trim().to_string()) + .collect(); + let msg: String = msg.trim().to_string(); + + broker + .send(Event::Message { + from: name.clone(), + to: dest, + msg, + }) + .await + .unwrap(); + } + + Ok(()) +} + +async fn connection_writer_loop( + messages: &mut Receiver, + stream: Arc, + mut shutdown: Receiver, +) -> Result<()> { + let mut stream = &*stream; + loop { + select! { + msg = messages.next().fuse() => match msg { + Some(msg) => stream.write_all(msg.as_bytes()).await?, + None => break, + }, + void = shutdown.next().fuse() => match void { + Some(void) => match void {}, + None => break, + } + } + } + Ok(()) +} + +#[derive(Debug)] +enum Event { + NewPeer { + name: String, + stream: Arc, + shutdown: Receiver, + }, + Message { + from: String, + to: Vec, + msg: String, + }, +} + +async fn broker_loop(mut events: Receiver) { + let (disconnect_sender, mut disconnect_receiver) = + mpsc::unbounded::<(String, Receiver)>(); + let mut peers: HashMap> = HashMap::new(); + + loop { + let event = select! { + event = events.next().fuse() => match event { + None => break, + Some(event) => event, + }, + disconnect = disconnect_receiver.next().fuse() => { + let (name, _pending_messages) = disconnect.unwrap(); + assert!(peers.remove(&name).is_some()); + continue; + }, + }; + match event { + Event::Message { from, to, msg } => { + for addr in to { + if let Some(peer) = peers.get_mut(&addr) { + let msg = format!("from {}: {}\n", from, msg); + peer.send(msg).await.unwrap(); + } + } + } + Event::NewPeer { + name, + stream, + shutdown, + } => match peers.entry(name.clone()) { + Entry::Occupied(..) => (), + Entry::Vacant(entry) => { + let (client_sender, mut client_receiver) = mpsc::unbounded(); + entry.insert(client_sender); + let mut disconnect_sender = disconnect_sender.clone(); + spawn_and_log_error(async move { + let res = + connection_writer_loop(&mut client_receiver, stream, shutdown).await; + disconnect_sender + .send((name, client_receiver)) + .await + .unwrap(); + res + }); + } + }, + } + } + drop(peers); + drop(disconnect_sender); + while let Some((_name, _pending_messages)) = disconnect_receiver.next().await {} +} + +fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +where + F: Future> + Send + 'static, +{ + task::spawn(async move { + if let Err(e) = fut.await { + eprintln!("{}", e) + } + }) +} \ No newline at end of file From 6b88699980302e70572b9e9773fd0005fc520ded Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Tue, 21 May 2024 21:20:00 +0000 Subject: [PATCH 02/36] Port to tokio (with some warnings left) --- exercise-solutions/async-chat/Cargo.toml | 3 +- exercise-solutions/async-chat/src/client.rs | 41 ++++++------ exercise-solutions/async-chat/src/server.rs | 71 ++++++++++----------- 3 files changed, 53 insertions(+), 62 deletions(-) diff --git a/exercise-solutions/async-chat/Cargo.toml b/exercise-solutions/async-chat/Cargo.toml index b7fa2c1c..90505301 100644 --- a/exercise-solutions/async-chat/Cargo.toml +++ b/exercise-solutions/async-chat/Cargo.toml @@ -4,5 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] -futures = "0.3" -async-std = "1.6" \ No newline at end of file +tokio = { version = "1", features = ["full"] } diff --git a/exercise-solutions/async-chat/src/client.rs b/exercise-solutions/async-chat/src/client.rs index 47e9a668..92221d46 100644 --- a/exercise-solutions/async-chat/src/client.rs +++ b/exercise-solutions/async-chat/src/client.rs @@ -1,44 +1,39 @@ - -use futures::select; -use futures::FutureExt; - -use async_std::{ - io::{stdin, BufReader}, +use tokio::{ + io::{stdin, BufReader, AsyncBufReadExt, AsyncWriteExt}, net::{TcpStream, ToSocketAddrs}, - prelude::*, - task, }; type Result = std::result::Result>; -pub(crate) fn main() -> Result<()> { - task::block_on(try_main("127.0.0.1:8080")) +#[tokio::main] +pub(crate) async fn main() -> Result<()> { + try_main("127.0.0.1:8080").await } async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { let stream = TcpStream::connect(addr).await?; - let (reader, mut writer) = (&stream, &stream); + let (reader, mut writer) = tokio::io::split(stream); let reader = BufReader::new(reader); - let mut lines_from_server = futures::StreamExt::fuse(reader.lines()); + let mut lines_from_server = reader.lines(); let stdin = BufReader::new(stdin()); - let mut lines_from_stdin = futures::StreamExt::fuse(stdin.lines()); + let mut lines_from_stdin = stdin.lines(); loop { - select! { - line = lines_from_server.next().fuse() => match line { - Some(line) => { - let line = line?; + tokio::select! { + line = lines_from_server.next_line() => match line { + Ok(Some(line)) => { println!("{}", line); }, - None => break, + Ok(None) => break, + Err(e) => eprintln!("Error {:?}:", e), }, - line = lines_from_stdin.next().fuse() => match line { - Some(line) => { - let line = line?; + line = lines_from_stdin.next_line() => match line { + Ok(Some(line)) => { writer.write_all(line.as_bytes()).await?; writer.write_all(b"\n").await?; - } - None => break, + }, + Ok(None) => break, + Err(e) => eprintln!("Error {:?}:", e), } } } diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index 0c24cf2d..d3cf3584 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -1,14 +1,15 @@ use std::{ collections::hash_map::{Entry, HashMap}, sync::Arc, + future::Future, }; -use futures::{channel::mpsc, select, FutureExt, SinkExt}; +use tokio::sync::mpsc; -use async_std::{ - io::BufReader, +use tokio::{ + io::{BufReader,AsyncWriteExt, AsyncBufReadExt}, net::{TcpListener, TcpStream, ToSocketAddrs}, - prelude::*, + net::tcp::{OwnedReadHalf, OwnedWriteHalf}, task, }; @@ -19,18 +20,18 @@ type Receiver = mpsc::UnboundedReceiver; #[derive(Debug)] enum Void {} -pub(crate) fn main() -> Result<()> { - task::block_on(accept_loop("127.0.0.1:8080")) +#[tokio::main] +pub(crate) async fn main() -> Result<()> { + accept_loop("127.0.0.1:8080").await } async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let listener = TcpListener::bind(addr).await?; - let (broker_sender, broker_receiver) = mpsc::unbounded(); + let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); let broker = task::spawn(broker_loop(broker_receiver)); - let mut incoming = listener.incoming(); - while let Some(stream) = incoming.next().await { - let stream = stream?; + + while let Ok((stream, socket_addr)) = listener.accept().await { println!("Accepting from: {}", stream.peer_addr()?); spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); } @@ -40,29 +41,28 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { } async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result<()> { - let stream = Arc::new(stream); - let reader = BufReader::new(&*stream); + let (reader, mut writer) = stream.into_split(); + let reader = BufReader::new(reader); let mut lines = reader.lines(); - let name = match lines.next().await { - None => return Err("peer disconnected immediately".into()), - Some(line) => line?, + let name = match lines.next_line().await { + Ok(Some(line)) => line, + Ok(None) => return Err("peer disconnected immediately".into()), + Err(e) => return Err(Box::new(e)), }; println!("user {} connected", name); - let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded::(); + let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded_channel::(); broker .send(Event::NewPeer { name: name.clone(), - stream: Arc::clone(&stream), + stream: writer, shutdown: shutdown_receiver, }) - .await .unwrap(); - while let Some(line) = lines.next().await { - let line = line?; + while let Ok(Some(line)) = lines.next_line().await { let (dest, msg) = match line.find(':') { None => continue, Some(idx) => (&line[..idx], line[idx + 1..].trim()), @@ -79,7 +79,6 @@ async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result to: dest, msg, }) - .await .unwrap(); } @@ -88,17 +87,16 @@ async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result async fn connection_writer_loop( messages: &mut Receiver, - stream: Arc, + stream: &mut OwnedWriteHalf, mut shutdown: Receiver, ) -> Result<()> { - let mut stream = &*stream; loop { - select! { - msg = messages.next().fuse() => match msg { + tokio::select! { + msg = messages.recv() => match msg { Some(msg) => stream.write_all(msg.as_bytes()).await?, None => break, }, - void = shutdown.next().fuse() => match void { + void = shutdown.recv() => match void { Some(void) => match void {}, None => break, } @@ -111,7 +109,7 @@ async fn connection_writer_loop( enum Event { NewPeer { name: String, - stream: Arc, + stream: OwnedWriteHalf, shutdown: Receiver, }, Message { @@ -123,16 +121,16 @@ enum Event { async fn broker_loop(mut events: Receiver) { let (disconnect_sender, mut disconnect_receiver) = - mpsc::unbounded::<(String, Receiver)>(); + mpsc::unbounded_channel::<(String, Receiver)>(); let mut peers: HashMap> = HashMap::new(); loop { - let event = select! { - event = events.next().fuse() => match event { + let event = tokio::select! { + event = events.recv() => match event { None => break, Some(event) => event, }, - disconnect = disconnect_receiver.next().fuse() => { + disconnect = disconnect_receiver.recv() => { let (name, _pending_messages) = disconnect.unwrap(); assert!(peers.remove(&name).is_some()); continue; @@ -143,26 +141,25 @@ async fn broker_loop(mut events: Receiver) { for addr in to { if let Some(peer) = peers.get_mut(&addr) { let msg = format!("from {}: {}\n", from, msg); - peer.send(msg).await.unwrap(); + peer.send(msg).unwrap(); } } } Event::NewPeer { name, - stream, + mut stream, shutdown, } => match peers.entry(name.clone()) { Entry::Occupied(..) => (), Entry::Vacant(entry) => { - let (client_sender, mut client_receiver) = mpsc::unbounded(); + let (client_sender, mut client_receiver) = mpsc::unbounded_channel(); entry.insert(client_sender); let mut disconnect_sender = disconnect_sender.clone(); spawn_and_log_error(async move { let res = - connection_writer_loop(&mut client_receiver, stream, shutdown).await; + connection_writer_loop(&mut client_receiver, &mut stream, shutdown).await; disconnect_sender .send((name, client_receiver)) - .await .unwrap(); res }); @@ -172,7 +169,7 @@ async fn broker_loop(mut events: Receiver) { } drop(peers); drop(disconnect_sender); - while let Some((_name, _pending_messages)) = disconnect_receiver.next().await {} + while let Some((_name, _pending_messages)) = disconnect_receiver.recv().await {} } fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> From ae380f6e00ae668c01b170086f4f48c2810b3916 Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Tue, 21 May 2024 21:52:27 +0000 Subject: [PATCH 03/36] Leave a small note --- exercise-solutions/async-chat/src/server.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index d3cf3584..d06e8a3f 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -93,6 +93,8 @@ async fn connection_writer_loop( loop { tokio::select! { msg = messages.recv() => match msg { + // TODO: explain why this works even with cancellation safety in mind + // hint: it's because there cannot be a concurrent shutdown message Some(msg) => stream.write_all(msg.as_bytes()).await?, None => break, }, From 6f69d4f6d8e530ca1e012d1a22fb8645f7a3bb63 Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Tue, 21 May 2024 22:16:02 +0000 Subject: [PATCH 04/36] Fix comment --- exercise-solutions/async-chat/src/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index d06e8a3f..085584f3 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -93,8 +93,8 @@ async fn connection_writer_loop( loop { tokio::select! { msg = messages.recv() => match msg { - // TODO: explain why this works even with cancellation safety in mind - // hint: it's because there cannot be a concurrent shutdown message + // TODO: fix this with a cancellation safe variant, pulling + // write_all out of the select Some(msg) => stream.write_all(msg.as_bytes()).await?, None => break, }, From 23e7addc22ee6c2231fe4f9dc748824d380aaa48 Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Thu, 23 May 2024 12:25:38 +0000 Subject: [PATCH 05/36] Make rustfmt happy --- exercise-solutions/async-chat/src/client.rs | 4 ++-- exercise-solutions/async-chat/src/main.rs | 2 +- exercise-solutions/async-chat/src/server.rs | 15 +++++++-------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/exercise-solutions/async-chat/src/client.rs b/exercise-solutions/async-chat/src/client.rs index 92221d46..3fb5b098 100644 --- a/exercise-solutions/async-chat/src/client.rs +++ b/exercise-solutions/async-chat/src/client.rs @@ -1,5 +1,5 @@ use tokio::{ - io::{stdin, BufReader, AsyncBufReadExt, AsyncWriteExt}, + io::{stdin, AsyncBufReadExt, AsyncWriteExt, BufReader}, net::{TcpStream, ToSocketAddrs}, }; @@ -38,4 +38,4 @@ async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { } } Ok(()) -} \ No newline at end of file +} diff --git a/exercise-solutions/async-chat/src/main.rs b/exercise-solutions/async-chat/src/main.rs index 1e30ccd3..89e5e2b6 100644 --- a/exercise-solutions/async-chat/src/main.rs +++ b/exercise-solutions/async-chat/src/main.rs @@ -10,4 +10,4 @@ fn main() -> Result<()> { (Some("server"), None) => server::main(), _ => Err("Usage: a-chat [client|server]".into()), } -} \ No newline at end of file +} diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index 085584f3..38a8859d 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -1,15 +1,15 @@ use std::{ collections::hash_map::{Entry, HashMap}, - sync::Arc, future::Future, + sync::Arc, }; use tokio::sync::mpsc; use tokio::{ - io::{BufReader,AsyncWriteExt, AsyncBufReadExt}, - net::{TcpListener, TcpStream, ToSocketAddrs}, + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, net::tcp::{OwnedReadHalf, OwnedWriteHalf}, + net::{TcpListener, TcpStream, ToSocketAddrs}, task, }; @@ -159,10 +159,9 @@ async fn broker_loop(mut events: Receiver) { let mut disconnect_sender = disconnect_sender.clone(); spawn_and_log_error(async move { let res = - connection_writer_loop(&mut client_receiver, &mut stream, shutdown).await; - disconnect_sender - .send((name, client_receiver)) - .unwrap(); + connection_writer_loop(&mut client_receiver, &mut stream, shutdown) + .await; + disconnect_sender.send((name, client_receiver)).unwrap(); res }); } @@ -183,4 +182,4 @@ where eprintln!("{}", e) } }) -} \ No newline at end of file +} From a37991f2a58525c75013aa61ea8c63c27fac2fa1 Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Sun, 26 May 2024 18:22:04 +0000 Subject: [PATCH 06/36] Turn shutdown channels into oneshots --- exercise-solutions/async-chat/src/server.rs | 31 ++++++++------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index 38a8859d..3134dbc9 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -1,14 +1,13 @@ use std::{ collections::hash_map::{Entry, HashMap}, future::Future, - sync::Arc, }; -use tokio::sync::mpsc; +use tokio::sync::{mpsc,oneshot}; use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - net::tcp::{OwnedReadHalf, OwnedWriteHalf}, + net::tcp::OwnedWriteHalf, net::{TcpListener, TcpStream, ToSocketAddrs}, task, }; @@ -17,9 +16,6 @@ type Result = std::result::Result type Sender = mpsc::UnboundedSender; type Receiver = mpsc::UnboundedReceiver; -#[derive(Debug)] -enum Void {} - #[tokio::main] pub(crate) async fn main() -> Result<()> { accept_loop("127.0.0.1:8080").await @@ -31,17 +27,17 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); let broker = task::spawn(broker_loop(broker_receiver)); - while let Ok((stream, socket_addr)) = listener.accept().await { + while let Ok((stream, _socket_addr)) = listener.accept().await { println!("Accepting from: {}", stream.peer_addr()?); spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); } drop(broker_sender); - broker.await; + broker.await?; Ok(()) } -async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result<()> { - let (reader, mut writer) = stream.into_split(); +async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { + let (reader, writer) = stream.into_split(); let reader = BufReader::new(reader); let mut lines = reader.lines(); @@ -53,7 +49,7 @@ async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result println!("user {} connected", name); - let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded_channel::(); + let (_shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); broker .send(Event::NewPeer { name: name.clone(), @@ -88,20 +84,15 @@ async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result async fn connection_writer_loop( messages: &mut Receiver, stream: &mut OwnedWriteHalf, - mut shutdown: Receiver, + mut shutdown: oneshot::Receiver<()>, ) -> Result<()> { loop { tokio::select! { msg = messages.recv() => match msg { - // TODO: fix this with a cancellation safe variant, pulling - // write_all out of the select Some(msg) => stream.write_all(msg.as_bytes()).await?, None => break, }, - void = shutdown.recv() => match void { - Some(void) => match void {}, - None => break, - } + _ = &mut shutdown => break } } Ok(()) @@ -112,7 +103,7 @@ enum Event { NewPeer { name: String, stream: OwnedWriteHalf, - shutdown: Receiver, + shutdown: oneshot::Receiver<()>, }, Message { from: String, @@ -156,7 +147,7 @@ async fn broker_loop(mut events: Receiver) { Entry::Vacant(entry) => { let (client_sender, mut client_receiver) = mpsc::unbounded_channel(); entry.insert(client_sender); - let mut disconnect_sender = disconnect_sender.clone(); + let disconnect_sender = disconnect_sender.clone(); spawn_and_log_error(async move { let res = connection_writer_loop(&mut client_receiver, &mut stream, shutdown) From 47debaeb6ea16c6137a610c4f00b43ae33512351 Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Sun, 26 May 2024 18:25:02 +0000 Subject: [PATCH 07/36] apply rustfmt --- exercise-solutions/async-chat/src/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index 3134dbc9..5c4ee448 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -3,7 +3,7 @@ use std::{ future::Future, }; -use tokio::sync::{mpsc,oneshot}; +use tokio::sync::{mpsc, oneshot}; use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, From 2d0e52a85886cc9e9eb582da95e4c8433070abd4 Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Sun, 26 May 2024 19:18:14 +0000 Subject: [PATCH 08/36] Add step1 template --- exercise-templates/Cargo.toml | 1 + .../async-chat/step1/Cargo.toml | 7 ++ .../async-chat/step1/src/client.rs | 41 +++++++ .../async-chat/step1/src/main.rs | 13 ++ .../async-chat/step1/src/server.rs | 112 ++++++++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 exercise-templates/async-chat/step1/Cargo.toml create mode 100644 exercise-templates/async-chat/step1/src/client.rs create mode 100644 exercise-templates/async-chat/step1/src/main.rs create mode 100644 exercise-templates/async-chat/step1/src/server.rs diff --git a/exercise-templates/Cargo.toml b/exercise-templates/Cargo.toml index 8c7fb876..c43ed95e 100644 --- a/exercise-templates/Cargo.toml +++ b/exercise-templates/Cargo.toml @@ -4,4 +4,5 @@ members = [ "urls-match-result", "rustlatin/*", "tcp-echo-server", + "async-chat/*", ] diff --git a/exercise-templates/async-chat/step1/Cargo.toml b/exercise-templates/async-chat/step1/Cargo.toml new file mode 100644 index 00000000..90505301 --- /dev/null +++ b/exercise-templates/async-chat/step1/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "async-chat" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["full"] } diff --git a/exercise-templates/async-chat/step1/src/client.rs b/exercise-templates/async-chat/step1/src/client.rs new file mode 100644 index 00000000..3fb5b098 --- /dev/null +++ b/exercise-templates/async-chat/step1/src/client.rs @@ -0,0 +1,41 @@ +use tokio::{ + io::{stdin, AsyncBufReadExt, AsyncWriteExt, BufReader}, + net::{TcpStream, ToSocketAddrs}, +}; + +type Result = std::result::Result>; + +#[tokio::main] +pub(crate) async fn main() -> Result<()> { + try_main("127.0.0.1:8080").await +} + +async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { + let stream = TcpStream::connect(addr).await?; + let (reader, mut writer) = tokio::io::split(stream); + let reader = BufReader::new(reader); + let mut lines_from_server = reader.lines(); + + let stdin = BufReader::new(stdin()); + let mut lines_from_stdin = stdin.lines(); + loop { + tokio::select! { + line = lines_from_server.next_line() => match line { + Ok(Some(line)) => { + println!("{}", line); + }, + Ok(None) => break, + Err(e) => eprintln!("Error {:?}:", e), + }, + line = lines_from_stdin.next_line() => match line { + Ok(Some(line)) => { + writer.write_all(line.as_bytes()).await?; + writer.write_all(b"\n").await?; + }, + Ok(None) => break, + Err(e) => eprintln!("Error {:?}:", e), + } + } + } + Ok(()) +} diff --git a/exercise-templates/async-chat/step1/src/main.rs b/exercise-templates/async-chat/step1/src/main.rs new file mode 100644 index 00000000..89e5e2b6 --- /dev/null +++ b/exercise-templates/async-chat/step1/src/main.rs @@ -0,0 +1,13 @@ +mod client; +mod server; + +type Result = std::result::Result>; + +fn main() -> Result<()> { + let mut args = std::env::args(); + match (args.nth(1).as_ref().map(String::as_str), args.next()) { + (Some("client"), None) => client::main(), + (Some("server"), None) => server::main(), + _ => Err("Usage: a-chat [client|server]".into()), + } +} diff --git a/exercise-templates/async-chat/step1/src/server.rs b/exercise-templates/async-chat/step1/src/server.rs new file mode 100644 index 00000000..0484e3ec --- /dev/null +++ b/exercise-templates/async-chat/step1/src/server.rs @@ -0,0 +1,112 @@ +use std::{ + collections::hash_map::{Entry, HashMap}, + future::Future, +}; + +use tokio::sync::{mpsc, oneshot}; + +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + net::tcp::OwnedWriteHalf, + net::{TcpListener, TcpStream, ToSocketAddrs}, + task, +}; + +type Result = std::result::Result>; +type Sender = mpsc::UnboundedSender; +type Receiver = mpsc::UnboundedReceiver; + +#[tokio::main] +pub(crate) async fn main() -> Result<()> { + accept_loop("127.0.0.1:8080").await +} + +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + let listener = TcpListener::bind(addr).await?; + + let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); + let broker = task::spawn(broker_loop(broker_receiver)); + + while let Ok((stream, _socket_addr)) = listener.accept().await { + println!("Accepting from: {}", stream.peer_addr()?); + spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); + } + drop(broker_sender); + broker.await?; + Ok(()) +} + +async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { + let (reader, writer) = stream.into_split(); + let reader = BufReader::new(reader); + let mut lines = reader.lines(); + + let name = match lines.next_line().await { + Ok(Some(line)) => line, + Ok(None) => return Err("peer disconnected immediately".into()), + Err(e) => return Err(Box::new(e)), + }; + + println!("user {} connected", name); + + while let Ok(Some(line)) = lines.next_line().await { + let (dest, msg) = match line.find(':') { + None => continue, + Some(idx) => (&line[..idx], line[idx + 1..].trim()), + }; + let dest: Vec = dest + .split(',') + .map(|name| name.trim().to_string()) + .collect(); + let msg: String = msg.trim().to_string(); + + println!("User {} sent message: {}", name, msg) + } + + Ok(()) +} + +async fn connection_writer_loop( + messages: &mut Receiver, + stream: &mut OwnedWriteHalf, + mut shutdown: oneshot::Receiver<()>, +) -> Result<()> { + loop { + let msg = messages.recv().await; + match msg { + Some(msg) => stream.write_all(msg.as_bytes()).await?, + None => break, + } + } + Ok(()) +} + +#[derive(Debug)] +enum Event { + NewPeer { + name: String, + stream: OwnedWriteHalf, + shutdown: oneshot::Receiver<()>, + }, + Message { + from: String, + to: Vec, + msg: String, + }, +} + +async fn broker_loop(mut events: Receiver) { + loop { + } +} + +fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +where + F: Future> + Send + 'static, +{ + task::spawn(async move { + if let Err(e) = fut.await { + eprintln!("{}", e) + } + }) +} From 11e4594a20e97abfbe0fd2a8b9ed6d76629ff7d4 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 10:18:39 +0200 Subject: [PATCH 09/36] Add async-std book --- exercise-solutions/async-chat/book/.gitignore | 1 + exercise-solutions/async-chat/book/book.toml | 13 + .../async-chat/book/src/SUMMARY.md | 26 ++ .../async-chat/book/src/concepts.md | 15 + .../book/src/concepts/async-read-write.md | 1 + .../async-chat/book/src/concepts/data.csv | 0 .../async-chat/book/src/concepts/futures.md | 143 ++++++++ .../async-chat/book/src/concepts/streams.md | 1 + .../async-chat/book/src/concepts/tasks.md | 150 +++++++++ .../async-chat/book/src/glossary.md | 7 + .../async-chat/book/src/images/async_1.svg | 165 ++++++++++ .../async-chat/book/src/images/async_2.svg | 66 ++++ .../book/src/images/horizontal_color.svg | 78 +++++ .../async-chat/book/src/images/icon_color.svg | 80 +++++ .../book/src/images/vertical_color.svg | 85 +++++ .../async-chat/book/src/introduction.md | 9 + .../async-chat/book/src/overview/async-std.md | 7 + .../book/src/overview/stability-guarantees.md | 40 +++ .../async-chat/book/src/patterns.md | 5 + .../book/src/patterns/accept-loop.md | 266 +++++++++++++++ .../patterns/accepting-concurrent-requests.md | 1 + .../book/src/patterns/async-read-write.md | 1 + .../book/src/patterns/background-tasks.md | 1 + .../async-chat/book/src/patterns/fork-join.md | 1 + .../book/src/patterns/proper-shutdown.md | 1 + .../book/src/patterns/small-patterns.md | 16 + .../async-chat/book/src/patterns/testing.md | 1 + .../async-chat/book/src/security/index.md | 12 + .../async-chat/book/src/security/policy.md | 66 ++++ .../book/src/tutorial/accept_loop.md | 96 ++++++ .../book/src/tutorial/all_together.md | 142 ++++++++ .../book/src/tutorial/clean_shutdown.md | 253 ++++++++++++++ .../connecting_readers_and_writers.md | 100 ++++++ .../src/tutorial/handling_disconnection.md | 308 ++++++++++++++++++ .../src/tutorial/implementing_a_client.md | 63 ++++ .../async-chat/book/src/tutorial/index.md | 14 + .../book/src/tutorial/receiving_messages.md | 145 +++++++++ .../book/src/tutorial/sending_messages.md | 44 +++ .../book/src/tutorial/specification.md | 47 +++ 39 files changed, 2470 insertions(+) create mode 100644 exercise-solutions/async-chat/book/.gitignore create mode 100644 exercise-solutions/async-chat/book/book.toml create mode 100644 exercise-solutions/async-chat/book/src/SUMMARY.md create mode 100644 exercise-solutions/async-chat/book/src/concepts.md create mode 100644 exercise-solutions/async-chat/book/src/concepts/async-read-write.md create mode 100644 exercise-solutions/async-chat/book/src/concepts/data.csv create mode 100644 exercise-solutions/async-chat/book/src/concepts/futures.md create mode 100644 exercise-solutions/async-chat/book/src/concepts/streams.md create mode 100644 exercise-solutions/async-chat/book/src/concepts/tasks.md create mode 100644 exercise-solutions/async-chat/book/src/glossary.md create mode 100644 exercise-solutions/async-chat/book/src/images/async_1.svg create mode 100644 exercise-solutions/async-chat/book/src/images/async_2.svg create mode 100644 exercise-solutions/async-chat/book/src/images/horizontal_color.svg create mode 100644 exercise-solutions/async-chat/book/src/images/icon_color.svg create mode 100644 exercise-solutions/async-chat/book/src/images/vertical_color.svg create mode 100644 exercise-solutions/async-chat/book/src/introduction.md create mode 100644 exercise-solutions/async-chat/book/src/overview/async-std.md create mode 100644 exercise-solutions/async-chat/book/src/overview/stability-guarantees.md create mode 100644 exercise-solutions/async-chat/book/src/patterns.md create mode 100644 exercise-solutions/async-chat/book/src/patterns/accept-loop.md create mode 100644 exercise-solutions/async-chat/book/src/patterns/accepting-concurrent-requests.md create mode 100644 exercise-solutions/async-chat/book/src/patterns/async-read-write.md create mode 100644 exercise-solutions/async-chat/book/src/patterns/background-tasks.md create mode 100644 exercise-solutions/async-chat/book/src/patterns/fork-join.md create mode 100644 exercise-solutions/async-chat/book/src/patterns/proper-shutdown.md create mode 100644 exercise-solutions/async-chat/book/src/patterns/small-patterns.md create mode 100644 exercise-solutions/async-chat/book/src/patterns/testing.md create mode 100644 exercise-solutions/async-chat/book/src/security/index.md create mode 100644 exercise-solutions/async-chat/book/src/security/policy.md create mode 100644 exercise-solutions/async-chat/book/src/tutorial/accept_loop.md create mode 100644 exercise-solutions/async-chat/book/src/tutorial/all_together.md create mode 100644 exercise-solutions/async-chat/book/src/tutorial/clean_shutdown.md create mode 100644 exercise-solutions/async-chat/book/src/tutorial/connecting_readers_and_writers.md create mode 100644 exercise-solutions/async-chat/book/src/tutorial/handling_disconnection.md create mode 100644 exercise-solutions/async-chat/book/src/tutorial/implementing_a_client.md create mode 100644 exercise-solutions/async-chat/book/src/tutorial/index.md create mode 100644 exercise-solutions/async-chat/book/src/tutorial/receiving_messages.md create mode 100644 exercise-solutions/async-chat/book/src/tutorial/sending_messages.md create mode 100644 exercise-solutions/async-chat/book/src/tutorial/specification.md diff --git a/exercise-solutions/async-chat/book/.gitignore b/exercise-solutions/async-chat/book/.gitignore new file mode 100644 index 00000000..7585238e --- /dev/null +++ b/exercise-solutions/async-chat/book/.gitignore @@ -0,0 +1 @@ +book diff --git a/exercise-solutions/async-chat/book/book.toml b/exercise-solutions/async-chat/book/book.toml new file mode 100644 index 00000000..215f872f --- /dev/null +++ b/exercise-solutions/async-chat/book/book.toml @@ -0,0 +1,13 @@ +[book] +authors = ["The async-std maintainers"] +language = "en" +multilingual = false +src = "src" +title = "Async programming in Rust with async-std" + +[build] +create-missing = false + +[output.html] +git-repository-url = "https://github.com/async-rs/async-std" +git-repository-icon = "fa-github" diff --git a/exercise-solutions/async-chat/book/src/SUMMARY.md b/exercise-solutions/async-chat/book/src/SUMMARY.md new file mode 100644 index 00000000..9e828f66 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/SUMMARY.md @@ -0,0 +1,26 @@ +# Summary + +- [Introduction](./introduction.md) + - [Welcome to `async-std`!](./overview/async-std.md) + - [Stability guarantees](./overview/stability-guarantees.md) +- [Async concepts using async-std](./concepts.md) + - [Futures](./concepts/futures.md) + - [Tasks](./concepts/tasks.md) + - [Async read/write](./concepts/async-read-write.md) + - [Streams and Channels](./concepts/streams.md) +- [Tutorial: Implementing a chat](./tutorial/index.md) + - [Specification and Getting started](./tutorial/specification.md) + - [Writing an Accept Loop](./tutorial/accept_loop.md) + - [Receiving Messages](./tutorial/receiving_messages.md) + - [Sending Messages](./tutorial/sending_messages.md) + - [Connecting Readers and Writers](./tutorial/connecting_readers_and_writers.md) + - [All Together](./tutorial/all_together.md) + - [Clean Shutdown](./tutorial/clean_shutdown.md) + - [Handling Disconnection](./tutorial/handling_disconnection.md) + - [Implementing a Client](./tutorial/implementing_a_client.md) +- [Async Patterns](./patterns.md) + - [TODO: Collected Small Patterns](./patterns/small-patterns.md) + - [Production-Ready Accept Loop](./patterns/accept-loop.md) +- [Security practices](./security/index.md) + - [Security Disclosures and Policy](./security/policy.md) +- [Glossary](./glossary.md) diff --git a/exercise-solutions/async-chat/book/src/concepts.md b/exercise-solutions/async-chat/book/src/concepts.md new file mode 100644 index 00000000..8e25cb12 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/concepts.md @@ -0,0 +1,15 @@ +# Async concepts using async-std + +[Rust Futures][futures] have the reputation of being hard. We don't think this is the case. They are, in our opinion, one of the easiest concurrency concepts around and have an intuitive explanation. + +However, there are good reasons for that perception. Futures have three concepts at their base that seem to be a constant source of confusion: deferred computation, asynchronicity and independence of execution strategy. + +These concepts are not hard, but something many people are not used to. This base confusion is amplified by many implementations oriented on details. Most explanations of these implementations also target advanced users, and can be hard for beginners. We try to provide both easy-to-understand primitives and approachable overviews of the concepts. + +Futures are a concept that abstracts over how code is run. By themselves, they do nothing. This is a weird concept in an imperative language, where usually one thing happens after the other - right now. + +So how do Futures run? You decide! Futures do nothing without the piece of code _executing_ them. This part is called an _executor_. An _executor_ decides _when_ and _how_ to execute your futures. The `async-std::task` module provides you with an interface to such an executor. + +Let's start with a little bit of motivation, though. + +[futures]: https://en.wikipedia.org/wiki/Futures_and_promises diff --git a/exercise-solutions/async-chat/book/src/concepts/async-read-write.md b/exercise-solutions/async-chat/book/src/concepts/async-read-write.md new file mode 100644 index 00000000..15675e99 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/concepts/async-read-write.md @@ -0,0 +1 @@ +# TODO: Async read/write diff --git a/exercise-solutions/async-chat/book/src/concepts/data.csv b/exercise-solutions/async-chat/book/src/concepts/data.csv new file mode 100644 index 00000000..e69de29b diff --git a/exercise-solutions/async-chat/book/src/concepts/futures.md b/exercise-solutions/async-chat/book/src/concepts/futures.md new file mode 100644 index 00000000..7d9cc636 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/concepts/futures.md @@ -0,0 +1,143 @@ +# Futures + +A notable point about Rust is [*fearless concurrency*](https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html). That is the notion that you should be empowered to do concurrent things, without giving up safety. Also, Rust being a low-level language, it's about fearless concurrency *without picking a specific implementation strategy*. This means we *must* abstract over the strategy, to allow choice *later*, if we want to have any way to share code between users of different strategies. + +Futures abstract over *computation*. They describe the "what", independent of the "where" and the "when". For that, they aim to break code into small, composable actions that can then be executed by a part of our system. Let's take a tour through what it means to compute things to find where we can abstract. + +## Send and Sync + +Luckily, concurrent Rust already has two well-known and effective concepts abstracting over sharing between concurrent parts of a program: `Send` and `Sync`. Notably, both the `Send` and `Sync` traits abstract over *strategies* of concurrent work, compose neatly, and don't prescribe an implementation. + +As a quick summary: + +- `Send` abstracts over *passing data* in a computation to another concurrent computation (let's call it the receiver), losing access to it on the sender side. In many programming languages, this strategy is commonly implemented, but missing support from the language side, and expects you to enforce the "losing access" behaviour yourself. This is a regular source of bugs: senders keeping handles to sent things around and maybe even working with them after sending. Rust mitigates this problem by making this behaviour known. Types can be `Send` or not (by implementing the appropriate marker trait), allowing or disallowing sending them around, and the ownership and borrowing rules prevent subsequent access. + +- `Sync` is about *sharing data* between two concurrent parts of a program. This is another common pattern: as writing to a memory location or reading while another party is writing is inherently unsafe, this access needs to be moderated through synchronisation.[^1] There are many common ways for two parties to agree on not using the same part in memory at the same time, for example mutexes and spinlocks. Again, Rust gives you the option of (safely!) not caring. Rust gives you the ability to express that something *needs* synchronisation while not being specific about the *how*. + +Note how we avoided any word like *"thread"*, but instead opted for "computation". The full power of `Send` and `Sync` is that they relieve you of the burden of knowing *what* shares. At the point of implementation, you only need to know which method of sharing is appropriate for the type at hand. This keeps reasoning local and is not influenced by whatever implementation the user of that type later uses. + +`Send` and `Sync` can be composed in interesting fashions, but that's beyond the scope here. You can find examples in the [Rust Book][rust-book-sync]. + +[rust-book-sync]: https://doc.rust-lang.org/stable/book/ch16-04-extensible-concurrency-sync-and-send.html + +To sum up: Rust gives us the ability to safely abstract over important properties of concurrent programs, their data sharing. It does so in a very lightweight fashion; the language itself only knows about the two markers `Send` and `Sync` and helps us a little by deriving them itself, when possible. The rest is a library concern. + +## An easy view of computation + +While computation is a subject to write a whole [book](https://computationbook.com/) about, a very simplified view suffices for us: A sequence of composable operations which can branch based on a decision, run to succession and yield a result or yield an error + +## Deferring computation + +As mentioned above, `Send` and `Sync` are about data. But programs are not only about data, they also talk about *computing* the data. And that's what [`Futures`][futures] do. We are going to have a close look at how that works in the next chapter. Let's look at what Futures allow us to express, in English. Futures go from this plan: + +- Do X +- If X succeeded, do Y + +towards: + +- Start doing X +- Once X succeeds, start doing Y + +Remember the talk about "deferred computation" in the intro? That's all it is. Instead of telling the computer what to execute and decide upon *now*, you tell it what to start doing and how to react on potential events in the... well... `Future`. + +[futures]: https://doc.rust-lang.org/std/future/trait.Future.html + +## Orienting towards the beginning + +Let's have a look at a simple function, specifically the return value: + +```rust,edition2018 +# use std::{fs::File, io, io::prelude::*}; +# +fn read_file(path: &str) -> io::Result { + let mut file = File::open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + Ok(contents) +} +``` + +You can call that at any time, so you are in full control on when you call it. But here's the problem: the moment you call it, you transfer control to the called function until it returns a value - eventually. +Note that this return value talks about the past. The past has a drawback: all decisions have been made. It has an advantage: the outcome is visible. We can unwrap the results of the program's past computation, and then decide what to do with it. + +But we wanted to abstract over *computation* and let someone else choose how to run it. That's fundamentally incompatible with looking at the results of previous computation all the time. So, let's find a type that *describes* a computation without running it. Let's look at the function again: + +```rust,edition2018 +# use std::{fs::File, io, io::prelude::*}; +# +fn read_file(path: &str) -> io::Result { + let mut file = File::open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + Ok(contents) +} +``` + +Speaking in terms of time, we can only take action *before* calling the function or *after* the function returned. This is not desirable, as it takes from us the ability to do something *while* it runs. When working with parallel code, this would take from us the ability to start a parallel task while the first runs (because we gave away control). + +This is the moment where we could reach for [threads](https://en.wikipedia.org/wiki/Thread_). But threads are a very specific concurrency primitive and we said that we are searching for an abstraction. + +What we are searching for is something that represents ongoing work towards a result in the future. Whenever we say "something" in Rust, we almost always mean a trait. Let's start with an incomplete definition of the `Future` trait: + +```rust,edition2018 +# use std::{pin::Pin, task::{Context, Poll}}; +# +trait Future { + type Output; + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll; +} +``` + +Looking at it closely, we see the following: + +- It is generic over the `Output`. +- It provides a function called `poll`, which allows us to check on the state of the current computation. +- (Ignore `Pin` and `Context` for now, you don't need them for high-level understanding.) + +Every call to `poll()` can result in one of these two cases: + +1. The computation is done, `poll` will return [`Poll::Ready`](https://doc.rust-lang.org/std/task/enum.Poll.html#variant.Ready) +2. The computation has not finished executing, it will return [`Poll::Pending`](https://doc.rust-lang.org/std/task/enum.Poll.html#variant.Pending) + +This allows us to externally check if a `Future` still has unfinished work, or is finally done and can give us the value. The most simple (but not efficient) way would be to just constantly poll futures in a loop. There are optimisations possible, and this is what a good runtime does for you. +Note that calling `poll` again after case 1 happened may result in confusing behaviour. See the [futures-docs](https://doc.rust-lang.org/std/future/trait.Future.html) for details. + +## Async + +While the `Future` trait has existed in Rust for a while, it was inconvenient to build and describe them. For this, Rust now has a special syntax: `async`. The example from above, implemented with `async-std`, would look like this: + +```rust,edition2018 +# extern crate async_std; +# use async_std::{fs::File, io, io::prelude::*}; +# +async fn read_file(path: &str) -> io::Result { + let mut file = File::open(path).await?; + let mut contents = String::new(); + file.read_to_string(&mut contents).await?; + Ok(contents) +} +``` + +Amazingly little difference, right? All we did is label the function `async` and insert 2 special commands: `.await`. + +This `async` function sets up a deferred computation. When this function is called, it will produce a `Future>` instead of immediately returning a `io::Result`. (Or, more precisely, generate a type for you that implements `Future>`.) + +## What does `.await` do? + +The `.await` postfix does exactly what it says on the tin: the moment you use it, the code will wait until the requested action (e.g. opening a file or reading all data in it) is finished. The `.await?` is not special, it's just the application of the `?` operator to the result of `.await`. So, what is gained over the initial code example? We're getting futures and then immediately waiting for them? + +The `.await` points act as a marker. Here, the code will wait for a `Future` to produce its value. How will a future finish? You don't need to care! The marker allows the component (usually called the “runtime”) in charge of *executing* this piece of code to take care of all the other things it has to do while the computation finishes. It will come back to this point when the operation you are doing in the background is done. This is why this style of programming is also called *evented programming*. We are waiting for *things to happen* (e.g. a file to be opened) and then react (by starting to read). + +When executing 2 or more of these functions at the same time, our runtime system is then able to fill the wait time with handling *all the other events* currently going on. + +## Conclusion + +Working from values, we searched for something that expresses *working towards a value available later*. From there, we talked about the concept of polling. + +A `Future` is any data type that does not represent a value, but the ability to *produce a value at some point in the future*. Implementations of this are very varied and detailed depending on use-case, but the interface is simple. + +Next, we will introduce you to `tasks`, which we will use to actually *run* Futures. + +[^1]: Two parties reading while it is guaranteed that no one is writing is always safe. + +[futures]: https://rust-lang.github.io/async-book/02_execution/02_future.html diff --git a/exercise-solutions/async-chat/book/src/concepts/streams.md b/exercise-solutions/async-chat/book/src/concepts/streams.md new file mode 100644 index 00000000..7f319c7c --- /dev/null +++ b/exercise-solutions/async-chat/book/src/concepts/streams.md @@ -0,0 +1 @@ +# TODO: Streams diff --git a/exercise-solutions/async-chat/book/src/concepts/tasks.md b/exercise-solutions/async-chat/book/src/concepts/tasks.md new file mode 100644 index 00000000..c3dbbe20 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/concepts/tasks.md @@ -0,0 +1,150 @@ +# Tasks + +Now that we know what Futures are, we want to run them! + +In `async-std`, the [`task`][tasks] module is responsible for this. The simplest way is using the `block_on` function: + +```rust,edition2018 +# extern crate async_std; +use async_std::{fs::File, io, prelude::*, task}; + +async fn read_file(path: &str) -> io::Result { + let mut file = File::open(path).await?; + let mut contents = String::new(); + file.read_to_string(&mut contents).await?; + Ok(contents) +} + +fn main() { + let reader_task = task::spawn(async { + let result = read_file("data.csv").await; + match result { + Ok(s) => println!("{}", s), + Err(e) => println!("Error reading file: {:?}", e) + } + }); + println!("Started task!"); + task::block_on(reader_task); + println!("Stopped task!"); +} +``` + +This asks the runtime baked into `async_std` to execute the code that reads a file. Let's go one by one, though, inside to outside. + +```rust,edition2018 +# extern crate async_std; +# use async_std::{fs::File, io, prelude::*, task}; +# +# async fn read_file(path: &str) -> io::Result { +# let mut file = File::open(path).await?; +# let mut contents = String::new(); +# file.read_to_string(&mut contents).await?; +# Ok(contents) +# } +# +async { + let result = read_file("data.csv").await; + match result { + Ok(s) => println!("{}", s), + Err(e) => println!("Error reading file: {:?}", e) + } +}; +``` + +This is an `async` *block*. Async blocks are necessary to call `async` functions, and will instruct the compiler to include all the relevant instructions to do so. In Rust, all blocks return a value and `async` blocks happen to return a value of the kind `Future`. + +But let's get to the interesting part: + +```rust,edition2018 +# extern crate async_std; +# use async_std::task; +task::spawn(async { }); +``` + +`spawn` takes a `Future` and starts running it on a `Task`. It returns a `JoinHandle`. Futures in Rust are sometimes called *cold* Futures. You need something that starts running them. To run a Future, there may be some additional bookkeeping required, e.g. whether it's running or finished, where it is being placed in memory and what the current state is. This bookkeeping part is abstracted away in a `Task`. + +A `Task` is similar to a `Thread`, with some minor differences: it will be scheduled by the program instead of the operating system kernel, and if it encounters a point where it needs to wait, the program itself is responsible for waking it up again. We'll talk a little bit about that later. An `async_std` task can also have a name and an ID, just like a thread. + +For now, it is enough to know that once you have `spawn`ed a task, it will continue running in the background. The `JoinHandle` is itself a future that will finish once the `Task` has run to conclusion. Much like with `threads` and the `join` function, we can now call `block_on` on the handle to *block* the program (or the calling thread, to be specific) and wait for it to finish. + +## Tasks in `async_std` + +Tasks in `async_std` are one of the core abstractions. Much like Rust's `thread`s, they provide some practical functionality over the raw concept. `Tasks` have a relationship to the runtime, but they are in themselves separate. `async_std` tasks have a number of desirable properties: + +- They are allocated in one single allocation +- All tasks have a *backchannel*, which allows them to propagate results and errors to the spawning task through the `JoinHandle` +- They carry useful metadata for debugging +- They support task local storage + +`async_std`s task API handles setup and teardown of a backing runtime for you and doesn't rely on a runtime being explicitly started. + +## Blocking + +`Task`s are assumed to run _concurrently_, potentially by sharing a thread of execution. This means that operations blocking an _operating system thread_, such as `std::thread::sleep` or io function from Rust's `std` library will _stop execution of all tasks sharing this thread_. Other libraries (such as database drivers) have similar behaviour. Note that _blocking the current thread_ is not in and of itself bad behaviour, just something that does not mix well with the concurrent execution model of `async-std`. Essentially, never do this: + +```rust,edition2018 +# extern crate async_std; +# use async_std::task; +fn main() { + task::block_on(async { + // this is std::fs, which blocks + std::fs::read_to_string("test_file"); + }) +} +``` + +If you want to mix operation kinds, consider putting such blocking operations on a separate `thread`. + +## Errors and panics + +Tasks report errors through normal patterns: If they are fallible, their `Output` should be of kind `Result`. + +In case of `panic`, behaviour differs depending on whether there's a reasonable part that addresses the `panic`. If not, the program _aborts_. + +In practice, that means that `block_on` propagates panics to the blocking component: + +```rust,edition2018,should_panic +# extern crate async_std; +# use async_std::task; +fn main() { + task::block_on(async { + panic!("test"); + }); +} +``` + +```text +thread 'async-task-driver' panicked at 'test', examples/panic.rs:8:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace. +``` + +While panicking a spawned task will abort: + +```rust,edition2018,should_panic +# extern crate async_std; +# use async_std::task; +# use std::time::Duration; +task::spawn(async { + panic!("test"); +}); + +task::block_on(async { + task::sleep(Duration::from_millis(10000)).await; +}) +``` + +```text +thread 'async-task-driver' panicked at 'test', examples/panic.rs:8:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace. +Aborted (core dumped) +``` + +That might seem odd at first, but the other option would be to silently ignore panics in spawned tasks. The current behaviour can be changed by catching panics in the spawned task and reacting with custom behaviour. This gives users the choice of panic handling strategy. + +## Conclusion + +`async_std` comes with a useful `Task` type that works with an API similar to `std::thread`. It covers error and panic behaviour in a structured and defined way. + +Tasks are separate concurrent units and sometimes they need to communicate. That's where `Stream`s come in. + +[tasks]: https://docs.rs/async-std/latest/async_std/task/index.html diff --git a/exercise-solutions/async-chat/book/src/glossary.md b/exercise-solutions/async-chat/book/src/glossary.md new file mode 100644 index 00000000..42d6b4d9 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/glossary.md @@ -0,0 +1,7 @@ +# Glossary + +### blocking + +"blocked" generally refers to conditions that keep a task from doing its work. For example, it might need data to be sent by a client before continuing. When tasks become blocked, usually, other tasks are scheduled. + +Sometimes you hear that you should never call "blocking functions" in an async context. What this refers to is functions that block the current thread and do not yield control back. This keeps the executor from using this thread to schedule another task. diff --git a/exercise-solutions/async-chat/book/src/images/async_1.svg b/exercise-solutions/async-chat/book/src/images/async_1.svg new file mode 100644 index 00000000..be14812a --- /dev/null +++ b/exercise-solutions/async-chat/book/src/images/async_1.svg @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T + + + + + + T + + + + + + + + + + + + + + + + + + + + ! + + + + + + + + + + + + + + + + + + + + + + + + + + + + T + + + + + + T + + + + + + T + + + + + + + T + + + + + + T + + + + + diff --git a/exercise-solutions/async-chat/book/src/images/async_2.svg b/exercise-solutions/async-chat/book/src/images/async_2.svg new file mode 100644 index 00000000..fff4b3a9 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/images/async_2.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + S + + + + + + + + + + + + + + + + ! + + + + + + + + + + + + + T + + + + + + T + + + + diff --git a/exercise-solutions/async-chat/book/src/images/horizontal_color.svg b/exercise-solutions/async-chat/book/src/images/horizontal_color.svg new file mode 100644 index 00000000..88bed32c --- /dev/null +++ b/exercise-solutions/async-chat/book/src/images/horizontal_color.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/exercise-solutions/async-chat/book/src/images/icon_color.svg b/exercise-solutions/async-chat/book/src/images/icon_color.svg new file mode 100644 index 00000000..90dccdee --- /dev/null +++ b/exercise-solutions/async-chat/book/src/images/icon_color.svg @@ -0,0 +1,80 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/exercise-solutions/async-chat/book/src/images/vertical_color.svg b/exercise-solutions/async-chat/book/src/images/vertical_color.svg new file mode 100644 index 00000000..14bd065c --- /dev/null +++ b/exercise-solutions/async-chat/book/src/images/vertical_color.svg @@ -0,0 +1,85 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/exercise-solutions/async-chat/book/src/introduction.md b/exercise-solutions/async-chat/book/src/introduction.md new file mode 100644 index 00000000..19498db2 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/introduction.md @@ -0,0 +1,9 @@ +# Introduction + +![async-std logo](./images/horizontal_color.svg) + +This book serves as high-level documentation for `async-std` and a way of learning async programming in Rust through it. As such, it focuses on the `async-std` API and the task model it gives you. + +Please note that the Rust project provides its own book on asynchronous programming, called ["Asynchronous Programming in Rust"][async-book], which we highly recommend reading along with this book, as it provides a different, wider view on the topic. + +[async-book]: https://rust-lang.github.io/async-book/ diff --git a/exercise-solutions/async-chat/book/src/overview/async-std.md b/exercise-solutions/async-chat/book/src/overview/async-std.md new file mode 100644 index 00000000..0086599f --- /dev/null +++ b/exercise-solutions/async-chat/book/src/overview/async-std.md @@ -0,0 +1,7 @@ +# Welcome to `async-std` + +`async-std`, along with its [supporting libraries][organization], is a library making your life in async programming easier. It provides fundamental implementations for downstream libraries and applications alike. The name reflects the approach of this library: it is as closely modeled to the Rust main standard library as possible, replacing all components by async counterparts. + +`async-std` provides an interface to all important primitives: filesystem operations, network operations and concurrency basics like timers. It also exposes a `task` in a model similar to the `thread` module found in the Rust standard lib. But it does not only include I/O primitives, but also `async/await` compatible versions of primitives like `Mutex`. + +[organization]: https://github.com/async-rs diff --git a/exercise-solutions/async-chat/book/src/overview/stability-guarantees.md b/exercise-solutions/async-chat/book/src/overview/stability-guarantees.md new file mode 100644 index 00000000..8c14e20f --- /dev/null +++ b/exercise-solutions/async-chat/book/src/overview/stability-guarantees.md @@ -0,0 +1,40 @@ +# Stability and SemVer + +`async-std` follows https://semver.org/. + +In short: we are versioning our software as `MAJOR.MINOR.PATCH`. We increase the: + +* MAJOR version when there are incompatible API changes, +* MINOR version when we introduce functionality in a backwards-compatible manner +* PATCH version when we make backwards-compatible bug fixes + +We will provide migration documentation between major versions. + +## Future expectations + +`async-std` uses its own implementations of the following concepts: + +* `Read` +* `Write` +* `Seek` +* `BufRead` +* `Stream` + +For integration with the ecosystem, all types implementing these traits also have an implementation of the corresponding interfaces in the `futures-rs` library. +Please note that our SemVer guarantees don't extend to usage of those interfaces. We expect those to be conservatively updated and in lockstep. + +## Minimum version policy + +The current tentative policy is that the minimum Rust version required to use this crate can be increased in minor version updates. For example, if `async-std` 1.0 requires Rust 1.37.0, then `async-std` 1.0.z for all values of z will also require Rust 1.37.0 or newer. However, `async-std` 1.y for y > 0 may require a newer minimum version of Rust. + +In general, this crate will be conservative with respect to the minimum supported version of Rust. With `async/await` being a new feature though, we will track changes in a measured pace initially. + +## Security fixes + +Security fixes will be applied to _all_ minor branches of this library in all _supported_ major revisions. This policy might change in the future, in which case we give a notice at least _3 months_ ahead. + +## Credits + +This policy is based on [BurntSushi's regex crate][regex-policy]. + +[regex-policy]: https://github.com/rust-lang/regex#minimum-rust-version-policy diff --git a/exercise-solutions/async-chat/book/src/patterns.md b/exercise-solutions/async-chat/book/src/patterns.md new file mode 100644 index 00000000..a19b81b4 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/patterns.md @@ -0,0 +1,5 @@ +# Patterns + +This section documents small, useful patterns. + +It is intended to be read at a glance, allowing you to get back when you have a problem. \ No newline at end of file diff --git a/exercise-solutions/async-chat/book/src/patterns/accept-loop.md b/exercise-solutions/async-chat/book/src/patterns/accept-loop.md new file mode 100644 index 00000000..4bc43e7d --- /dev/null +++ b/exercise-solutions/async-chat/book/src/patterns/accept-loop.md @@ -0,0 +1,266 @@ +# Production-Ready Accept Loop + +A production-ready accept loop needs the following things: +1. Handling errors +2. Limiting the number of simultanteous connections to avoid deny-of-service + (DoS) attacks + + +## Handling errors + +There are two kinds of errors in an accept loop: +1. Per-connection errors. The system uses them to notify that there was a + connection in the queue and it's dropped by the peer. Subsequent connections + can be already queued so next connection must be accepted immediately. +2. Resource shortages. When these are encountered it doesn't make sense to + accept the next socket immediately. But the listener stays active, so you server + should try to accept socket later. + +Here is the example of a per-connection error (printed in normal and debug mode): +``` +Error: Connection reset by peer (os error 104) +Error: Os { code: 104, kind: ConnectionReset, message: "Connection reset by peer" } +``` + +And the following is the most common example of a resource shortage error: +``` +Error: Too many open files (os error 24) +Error: Os { code: 24, kind: Other, message: "Too many open files" } +``` + +### Testing Application + +To test your application for these errors try the following (this works +on unixes only). + +Lower limits and start the application: +``` +$ ulimit -n 100 +$ cargo run --example your_app + Compiling your_app v0.1.0 (/work) + Finished dev [unoptimized + debuginfo] target(s) in 5.47s + Running `target/debug/examples/your_app` +Server is listening on: http://127.0.0.1:1234 +``` +Then in another console run the [`wrk`] benchmark tool: +``` +$ wrk -c 1000 http://127.0.0.1:1234 +Running 10s test @ http://localhost:8080/ + 2 threads and 1000 connections +$ telnet localhost 1234 +Trying ::1... +Connected to localhost. +``` + +Important is to check the following things: + +1. The application doesn't crash on error (but may log errors, see below) +2. It's possible to connect to the application again once load is stopped + (few seconds after `wrk`). This is what `telnet` does in example above, + make sure it prints `Connected to `. +3. The `Too many open files` error is logged in the appropriate log. This + requires to set "maximum number of simultaneous connections" parameter (see + below) of your application to a value greater then `100` for this example. +4. Check CPU usage of the app while doing a test. It should not occupy 100% + of a single CPU core (it's unlikely that you can exhaust CPU by 1000 + connections in Rust, so this means error handling is not right). + +#### Testing non-HTTP applications + +If it's possible, use the appropriate benchmark tool and set the appropriate +number of connections. For example `redis-benchmark` has a `-c` parameter for +that, if you implement redis protocol. + +Alternatively, can still use `wrk`, just make sure that connection is not +immediately closed. If it is, put a temporary timeout before handing +the connection to the protocol handler, like this: + +```rust,edition2018 +# extern crate async_std; +# use std::time::Duration; +# use async_std::{ +# net::{TcpListener, ToSocketAddrs}, +# prelude::*, +# }; +# +# type Result = std::result::Result>; +# +#async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { +# let listener = TcpListener::bind(addr).await?; +# let mut incoming = listener.incoming(); +while let Some(stream) = incoming.next().await { + task::spawn(async { + task::sleep(Duration::from_secs(10)).await; // 1 + connection_loop(stream).await; + }); +} +# Ok(()) +# } +``` + +1. Make sure the sleep coroutine is inside the spawned task, not in the loop. + +[`wrk`]: https://github.com/wg/wrk + + +### Handling Errors Manually + +Here is how basic accept loop could look like: + +```rust,edition2018 +# extern crate async_std; +# use std::time::Duration; +# use async_std::{ +# net::{TcpListener, ToSocketAddrs}, +# prelude::*, +# }; +# +# type Result = std::result::Result>; +# +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + let listener = TcpListener::bind(addr).await?; + let mut incoming = listener.incoming(); + while let Some(result) = incoming.next().await { + let stream = match result { + Err(ref e) if is_connection_error(e) => continue, // 1 + Err(e) => { + eprintln!("Error: {}. Pausing for 500ms.", e); // 3 + task::sleep(Duration::from_millis(500)).await; // 2 + continue; + } + Ok(s) => s, + }; + // body + } + Ok(()) +} +``` + +1. Ignore per-connection errors. +2. Sleep and continue on resource shortage. +3. It's important to log the message, because these errors commonly mean the + misconfiguration of the system and are helpful for operations people running + the application. + +Be sure to [test your application](#testing-application). + + +### External Crates + +The crate [`async-listen`] has a helper to achieve this task: +```rust,edition2018 +# extern crate async_std; +# extern crate async_listen; +# use std::time::Duration; +# use async_std::{ +# net::{TcpListener, ToSocketAddrs}, +# prelude::*, +# }; +# +# type Result = std::result::Result>; +# +use async_listen::{ListenExt, error_hint}; + +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + + let listener = TcpListener::bind(addr).await?; + let mut incoming = listener + .incoming() + .log_warnings(log_accept_error) // 1 + .handle_errors(Duration::from_millis(500)); + while let Some(socket) = incoming.next().await { // 2 + // body + } + Ok(()) +} + +fn log_accept_error(e: &io::Error) { + eprintln!("Error: {}. Listener paused for 0.5s. {}", e, error_hint(e)) // 3 +} +``` + +1. Logs resource shortages (`async-listen` calls them warnings). If you use + `log` crate or any other in your app this should go to the log. +2. Stream yields sockets without `Result` wrapper after `handle_errors` because + all errors are already handled. +3. Together with the error we print a hint, which explains some errors for end + users. For example, it recommends increasing open file limit and gives + a link. + +[`async-listen`]: https://crates.io/crates/async-listen/ + +Be sure to [test your application](#testing-application). + + +## Connections Limit + +Even if you've applied everything described in +[Handling Errors](#handling-errors) section, there is still a problem. + +Let's imagine you have a server that needs to open a file to process +client request. At some point, you might encounter the following situation: + +1. There are as many client connection as max file descriptors allowed for + the application. +2. Listener gets `Too many open files` error so it sleeps. +3. Some client sends a request via the previously open connection. +4. Opening a file to serve request fails, because of the same + `Too many open files` error, until some other client drops a connection. + +There are many more possible situations, this is just a small illustation that +limiting number of connections is very useful. Generally, it's one of the ways +to control resources used by a server and avoiding some kinds of deny of +service (DoS) attacks. + +### `async-listen` crate + +Limiting maximum number of simultaneous connections with [`async-listen`] +looks like the following: + +```rust,edition2018 +# extern crate async_std; +# extern crate async_listen; +# use std::time::Duration; +# use async_std::{ +# net::{TcpListener, TcpStream, ToSocketAddrs}, +# prelude::*, +# }; +# +# type Result = std::result::Result>; +# +use async_listen::{ListenExt, Token, error_hint}; + +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + + let listener = TcpListener::bind(addr).await?; + let mut incoming = listener + .incoming() + .log_warnings(log_accept_error) + .handle_errors(Duration::from_millis(500)) // 1 + .backpressure(100); + while let Some((token, socket)) = incoming.next().await { // 2 + task::spawn(async move { + connection_loop(&token, stream).await; // 3 + }); + } + Ok(()) +} +async fn connection_loop(_token: &Token, stream: TcpStream) { // 4 + // ... +} +# fn log_accept_error(e: &io::Error) { +# eprintln!("Error: {}. Listener paused for 0.5s. {}", e, error_hint(e)); +# } +``` + +1. We need to handle errors first, because [`backpressure`] helper expects + stream of `TcpStream` rather than `Result`. +2. The token yielded by a new stream is what is counted by backpressure helper. + I.e. if you drop a token, new connection can be established. +3. We give the connection loop a reference to token to bind token's lifetime to + the lifetime of the connection. +4. The token itsellf in the function can be ignored, hence `_token` + +[`backpressure`]: https://docs.rs/async-listen/0.1.2/async_listen/trait.ListenExt.html#method.backpressure + +Be sure to [test this behavior](#testing-application). diff --git a/exercise-solutions/async-chat/book/src/patterns/accepting-concurrent-requests.md b/exercise-solutions/async-chat/book/src/patterns/accepting-concurrent-requests.md new file mode 100644 index 00000000..b984183c --- /dev/null +++ b/exercise-solutions/async-chat/book/src/patterns/accepting-concurrent-requests.md @@ -0,0 +1 @@ +# Accepting requests diff --git a/exercise-solutions/async-chat/book/src/patterns/async-read-write.md b/exercise-solutions/async-chat/book/src/patterns/async-read-write.md new file mode 100644 index 00000000..79354298 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/patterns/async-read-write.md @@ -0,0 +1 @@ +# Async read/write diff --git a/exercise-solutions/async-chat/book/src/patterns/background-tasks.md b/exercise-solutions/async-chat/book/src/patterns/background-tasks.md new file mode 100644 index 00000000..02571b26 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/patterns/background-tasks.md @@ -0,0 +1 @@ +# Background Tasks diff --git a/exercise-solutions/async-chat/book/src/patterns/fork-join.md b/exercise-solutions/async-chat/book/src/patterns/fork-join.md new file mode 100644 index 00000000..4709bb20 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/patterns/fork-join.md @@ -0,0 +1 @@ +# Fork/Join diff --git a/exercise-solutions/async-chat/book/src/patterns/proper-shutdown.md b/exercise-solutions/async-chat/book/src/patterns/proper-shutdown.md new file mode 100644 index 00000000..49ba1f43 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/patterns/proper-shutdown.md @@ -0,0 +1 @@ +# Proper Shutdown diff --git a/exercise-solutions/async-chat/book/src/patterns/small-patterns.md b/exercise-solutions/async-chat/book/src/patterns/small-patterns.md new file mode 100644 index 00000000..1bc1d907 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/patterns/small-patterns.md @@ -0,0 +1,16 @@ +# Small Patterns + +A collection of small, useful patterns. + +## Splitting streams + +`async-std` doesn't provide a `split()` method on `io` handles. Instead, splitting a stream into a read and write half can be done like this: + +```rust,edition2018 +# extern crate async_std; +use async_std::{io, net::TcpStream}; +async fn echo(stream: TcpStream) { + let (reader, writer) = &mut (&stream, &stream); + io::copy(reader, writer).await; +} +``` diff --git a/exercise-solutions/async-chat/book/src/patterns/testing.md b/exercise-solutions/async-chat/book/src/patterns/testing.md new file mode 100644 index 00000000..f00b526a --- /dev/null +++ b/exercise-solutions/async-chat/book/src/patterns/testing.md @@ -0,0 +1 @@ +# Testing diff --git a/exercise-solutions/async-chat/book/src/security/index.md b/exercise-solutions/async-chat/book/src/security/index.md new file mode 100644 index 00000000..02fef4c0 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/security/index.md @@ -0,0 +1,12 @@ +# Security + +Writing a highly performant async core library is a task involving some instances of unsafe code. + +We take great care in vetting all unsafe code included in `async-std` and do follow generally accepted practices. + +In the case that you find a security-related bug in our library, please get in touch with our [security contact][security-policy]. + +Patches improving the resilience of the library or the testing setup are happily accepted on our [github org][github]. + +[security-policy]: /security/policy +[github]: https://github.com/async-rs diff --git a/exercise-solutions/async-chat/book/src/security/policy.md b/exercise-solutions/async-chat/book/src/security/policy.md new file mode 100644 index 00000000..06a08b48 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/security/policy.md @@ -0,0 +1,66 @@ +# Policy + +Safety is one of the core principles of what we do, and to that end, we would like to ensure that async-std has a secure implementation. Thank you for taking the time to responsibly disclose any issues you find. + +All security bugs in async-std distribution should be reported by email to florian.gilcher@ferrous-systems.com. This list is delivered to a small security team. Your email will be acknowledged within 24 hours, and you’ll receive a more detailed response to your email within 48 hours indicating the next steps in handling your report. If you would like, you can encrypt your report using our public key. This key is also On MIT’s keyserver and reproduced below. + +Be sure to use a descriptive subject line to avoid having your report be missed. After the initial reply to your report, the security team will endeavor to keep you informed of the progress being made towards a fix and full announcement. As recommended by [RFPolicy][rf-policy], these updates will be sent at least every five days. In reality, this is more likely to be every 24-48 hours. + +If you have not received a reply to your email within 48 hours, or have not heard from the security team for the past five days, there are a few steps you can take (in order): + +* Post on our Community forums + +Please note that the discussion forums are public areas. When escalating in these venues, please do not discuss your issue. Simply say that you’re trying to get a hold of someone from the security team. + +[rf-policy]: https://en.wikipedia.org/wiki/RFPolicy + +## Disclosure policy + +The async-std project has a 5 step disclosure process. + +* The security report is received and is assigned a primary handler. This person will coordinate the fix and release process. +* The problem is confirmed and a list of all affected versions is determined. +* Code is audited to find any potential similar problems. +* Fixes are prepared for all releases which are still under maintenance. These fixes are not committed to the public repository but rather held locally pending the announcement. +* On the embargo date, the changes are pushed to the public repository and new builds are deployed to crates.io. Within 6 hours, a copy of the advisory will be published on the the async.rs blog. + +This process can take some time, especially when coordination is required with maintainers of other projects. Every effort will be made to handle the bug in as timely a manner as possible, however it's important that we follow the release process above to ensure that the disclosure is handled in a consistent manner. + +## Credits + +This policy is adapted from the [Rust project](https://www.rust-lang.org/policies/security) security policy. + +## PGP Key + +```text +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBF1Wu/ABCADJaGt4HwSlqKB9BGHWYKZj/6mTMbmc29vsEOcCSQKo6myCf9zc +sasWAttep4FAUDX+MJhVbBTSq9M1YVxp33Qh5AF0t9SnJZnbI+BZuGawcHDL01xE +bE+8bcA2+szeTTUZCeWwsaoTd/2qmQKvpUCBQp7uBs/ITO/I2q7+xCGXaOHZwUKc +H8SUBLd35nYFtjXAeejoZVkqG2qEjrc9bkZAwxFXi7Fw94QdkNLaCjNfKxZON/qP +A3WOpyWPr3ERk5C5prjEAvrW8kdqpTRjdmzQjsr8UEXb5GGEOo93N4OLZVQ2mXt9 +dfn++GOnOk7sTxvfiDH8Ru5o4zCtKgO+r5/LABEBAAG0UkZsb3JpYW4gR2lsY2hl +ciAoU2VjdXJpdHkgY29udGFjdCBhc3luYy1zdGQpIDxmbG9yaWFuLmdpbGNoZXJA +ZmVycm91cy1zeXN0ZW1zLmNvbT6JATgEEwECACIFAl1Wu/ACGwMGCwkIBwMCBhUI +AgkKCwQWAgMBAh4BAheAAAoJEACXY97PwLtSc0AH/18yvrElVOkG0ADWX7l+JKHH +nMQtYj0Auop8d6TuKBbpwtYhwELrQoITDMV7f2XEnchNsvYxAyBZhIISmXeJboE1 +KzZD1O+4QPXRcXhj+QNsKQ680mrgZXgAI2Y4ptIW9Vyw3jiHu/ZVopvDAt4li+up +3fRJGPAvGu+tclpJmA+Xam23cDj89M7/wHHgKIyT59WgFwyCgibL+NHKwg2Unzou +9uyZQnq6hf62sQTWEZIAr9BQpKmluplNIJHDeECWzZoE9ucE2ZXsq5pq9qojsAMK +yRdaFdpBcD/AxtrTKFeXGS7X7LqaljY/IFBEdJOqVNWpqSLjGWqjSLIEsc1AB0K5 +AQ0EXVa78AEIAJMxBOEEW+2c3CcjFuUfcRsoBsFH3Vk+GwCbjIpNHq/eAvS1yy2L +u10U5CcT5Xb6be3AeCYv00ZHVbEi6VwoauVCSX8qDjhVzQxvNLgQ1SduobjyF6t8 +3M/wTija6NvMKszyw1l2oHepxSMLej1m49DyCDFNiZm5rjQcYnFT4J71syxViqHF +v2fWCheTrHP3wfBAt5zyDet7IZd/EhYAK6xXEwr9nBPjfbaVexm2B8K6hOPNj0Bp +OKm4rcOj7JYlcxrwhMvNnwEue7MqH1oXAsoaC1BW+qs4acp/hHpesweL6Rcg1pED +OJUQd3UvRsqRK0EsorDu0oj5wt6Qp3ZEbPMAEQEAAYkBHwQYAQIACQUCXVa78AIb +DAAKCRAAl2Pez8C7Uv8bB/9scRm2wvzHLbFtcEHaHvlKO1yYfSVqKqJzIKHc7pM2 ++szM8JVRTxAbzK5Xih9SB5xlekixxO2UCJI5DkJ/ir/RCcg+/CAQ8iLm2UcYAgJD +TocKiR5gjNAvUDI4tMrDLLdF+7+RCQGc7HBSxFiNBJVGAztGVh1+cQ0zaCX6Tt33 +1EQtyRcPID0m6+ip5tCJN0dILC0YcwzXGrSgjB03JqItIyJEucdQz6UB84TIAGku +JJl4tktgD9T7Rb5uzRhHCSbLy89DQVvCcKD4B94ffuDW3HO8n8utDusOiZuG4BUf +WdFy6/gTLNiFbTzkq1BBJQMN1nBwGs1sn63RRgjumZ1N +=dIcF +-----END PGP PUBLIC KEY BLOCK----- +``` diff --git a/exercise-solutions/async-chat/book/src/tutorial/accept_loop.md b/exercise-solutions/async-chat/book/src/tutorial/accept_loop.md new file mode 100644 index 00000000..dc748bb4 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/tutorial/accept_loop.md @@ -0,0 +1,96 @@ +## Writing an Accept Loop + +Let's implement the scaffold of the server: a loop that binds a TCP socket to an address and starts accepting connections. + +First of all, let's add required import boilerplate: + +```rust,edition2018 +# extern crate async_std; +use async_std::{ + prelude::*, // 1 + task, // 2 + net::{TcpListener, ToSocketAddrs}, // 3 +}; + +type Result = std::result::Result>; // 4 +``` + +1. `prelude` re-exports some traits required to work with futures and streams. +2. The `task` module roughly corresponds to the `std::thread` module, but tasks are much lighter weight. + A single thread can run many tasks. +3. For the socket type, we use `TcpListener` from `async_std`, which is just like `std::net::TcpListener`, but is non-blocking and uses `async` API. +4. We will skip implementing comprehensive error handling in this example. + To propagate the errors, we will use a boxed error trait object. + Do you know that there's `From<&'_ str> for Box` implementation in stdlib, which allows you to use strings with `?` operator? + +Now we can write the server's accept loop: + +```rust,edition2018 +# extern crate async_std; +# use async_std::{ +# net::{TcpListener, ToSocketAddrs}, +# prelude::*, +# }; +# +# type Result = std::result::Result>; +# +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { // 1 + + let listener = TcpListener::bind(addr).await?; // 2 + let mut incoming = listener.incoming(); + while let Some(stream) = incoming.next().await { // 3 + // TODO + } + Ok(()) +} +``` + +1. We mark the `accept_loop` function as `async`, which allows us to use `.await` syntax inside. +2. `TcpListener::bind` call returns a future, which we `.await` to extract the `Result`, and then `?` to get a `TcpListener`. + Note how `.await` and `?` work nicely together. + This is exactly how `std::net::TcpListener` works, but with `.await` added. + Mirroring API of `std` is an explicit design goal of `async_std`. +3. Here, we would like to iterate incoming sockets, just how one would do in `std`: + +```rust,edition2018,should_panic +let listener: std::net::TcpListener = unimplemented!(); +for stream in listener.incoming() { +} +``` + +Unfortunately this doesn't quite work with `async` yet, because there's no support for `async` for-loops in the language yet. +For this reason we have to implement the loop manually, by using `while let Some(item) = iter.next().await` pattern. + +Finally, let's add main: + +```rust,edition2018 +# extern crate async_std; +# use async_std::{ +# net::{TcpListener, ToSocketAddrs}, +# prelude::*, +# task, +# }; +# +# type Result = std::result::Result>; +# +# async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { // 1 +# let listener = TcpListener::bind(addr).await?; // 2 +# let mut incoming = listener.incoming(); +# while let Some(stream) = incoming.next().await { // 3 +# // TODO +# } +# Ok(()) +# } +# +// main +fn run() -> Result<()> { + let fut = accept_loop("127.0.0.1:8080"); + task::block_on(fut) +} +``` + +The crucial thing to realise that is in Rust, unlike other languages, calling an async function does **not** run any code. +Async functions only construct futures, which are inert state machines. +To start stepping through the future state-machine in an async function, you should use `.await`. +In a non-async function, a way to execute a future is to hand it to the executor. +In this case, we use `task::block_on` to execute a future on the current thread and block until it's done. diff --git a/exercise-solutions/async-chat/book/src/tutorial/all_together.md b/exercise-solutions/async-chat/book/src/tutorial/all_together.md new file mode 100644 index 00000000..b8174d30 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/tutorial/all_together.md @@ -0,0 +1,142 @@ +## All Together + +At this point, we only need to start the broker to get a fully-functioning (in the happy case!) chat: + +```rust,edition2018 +# extern crate async_std; +# extern crate futures; +use async_std::{ + io::BufReader, + net::{TcpListener, TcpStream, ToSocketAddrs}, + prelude::*, + task, +}; +use futures::channel::mpsc; +use futures::sink::SinkExt; +use std::{ + collections::hash_map::{HashMap, Entry}, + sync::Arc, +}; + +type Result = std::result::Result>; +type Sender = mpsc::UnboundedSender; +type Receiver = mpsc::UnboundedReceiver; + +// main +fn run() -> Result<()> { + task::block_on(accept_loop("127.0.0.1:8080")) +} + +fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +where + F: Future> + Send + 'static, +{ + task::spawn(async move { + if let Err(e) = fut.await { + eprintln!("{}", e) + } + }) +} + +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + let listener = TcpListener::bind(addr).await?; + + let (broker_sender, broker_receiver) = mpsc::unbounded(); // 1 + let _broker_handle = task::spawn(broker_loop(broker_receiver)); + let mut incoming = listener.incoming(); + while let Some(stream) = incoming.next().await { + let stream = stream?; + println!("Accepting from: {}", stream.peer_addr()?); + spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); + } + Ok(()) +} + +async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result<()> { + let stream = Arc::new(stream); // 2 + let reader = BufReader::new(&*stream); + let mut lines = reader.lines(); + + let name = match lines.next().await { + None => Err("peer disconnected immediately")?, + Some(line) => line?, + }; + broker.send(Event::NewPeer { name: name.clone(), stream: Arc::clone(&stream) }).await // 3 + .unwrap(); + + while let Some(line) = lines.next().await { + let line = line?; + let (dest, msg) = match line.find(':') { + None => continue, + Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), + }; + let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); + let msg: String = msg.to_string(); + + broker.send(Event::Message { // 4 + from: name.clone(), + to: dest, + msg, + }).await.unwrap(); + } + Ok(()) +} + +async fn connection_writer_loop( + mut messages: Receiver, + stream: Arc, +) -> Result<()> { + let mut stream = &*stream; + while let Some(msg) = messages.next().await { + stream.write_all(msg.as_bytes()).await?; + } + Ok(()) +} + +#[derive(Debug)] +enum Event { + NewPeer { + name: String, + stream: Arc, + }, + Message { + from: String, + to: Vec, + msg: String, + }, +} + +async fn broker_loop(mut events: Receiver) -> Result<()> { + let mut peers: HashMap> = HashMap::new(); + + while let Some(event) = events.next().await { + match event { + Event::Message { from, to, msg } => { + for addr in to { + if let Some(peer) = peers.get_mut(&addr) { + let msg = format!("from {}: {}\n", from, msg); + peer.send(msg).await? + } + } + } + Event::NewPeer { name, stream} => { + match peers.entry(name) { + Entry::Occupied(..) => (), + Entry::Vacant(entry) => { + let (client_sender, client_receiver) = mpsc::unbounded(); + entry.insert(client_sender); + spawn_and_log_error(connection_writer_loop(client_receiver, stream)); + } + } + } + } + } + Ok(()) +} +``` + +1. Inside the `accept_loop`, we create the broker's channel and `task`. +2. Inside `connection_loop`, we need to wrap `TcpStream` into an `Arc`, to be able to share it with the `connection_writer_loop`. +3. On login, we notify the broker. + Note that we `.unwrap` on send: broker should outlive all the clients and if that's not the case the broker probably panicked, so we can escalate the panic as well. +4. Similarly, we forward parsed messages to the broker, assuming that it is alive. diff --git a/exercise-solutions/async-chat/book/src/tutorial/clean_shutdown.md b/exercise-solutions/async-chat/book/src/tutorial/clean_shutdown.md new file mode 100644 index 00000000..0937d708 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/tutorial/clean_shutdown.md @@ -0,0 +1,253 @@ +## Clean Shutdown + +One of the problems of the current implementation is that it doesn't handle graceful shutdown. +If we break from the accept loop for some reason, all in-flight tasks are just dropped on the floor. +A more correct shutdown sequence would be: + +1. Stop accepting new clients +2. Deliver all pending messages +3. Exit the process + +A clean shutdown in a channel based architecture is easy, although it can appear a magic trick at first. +In Rust, receiver side of a channel is closed as soon as all senders are dropped. +That is, as soon as producers exit and drop their senders, the rest of the system shuts down naturally. +In `async_std` this translates to two rules: + +1. Make sure that channels form an acyclic graph. +2. Take care to wait, in the correct order, until intermediate layers of the system process pending messages. + +In `a-chat`, we already have an unidirectional flow of messages: `reader -> broker -> writer`. +However, we never wait for broker and writers, which might cause some messages to get dropped. +Let's add waiting to the server: + +```rust,edition2018 +# extern crate async_std; +# extern crate futures; +# use async_std::{ +# io::{self, BufReader}, +# net::{TcpListener, TcpStream, ToSocketAddrs}, +# prelude::*, +# task, +# }; +# use futures::channel::mpsc; +# use futures::sink::SinkExt; +# use std::{ +# collections::hash_map::{HashMap, Entry}, +# sync::Arc, +# }; +# +# type Result = std::result::Result>; +# type Sender = mpsc::UnboundedSender; +# type Receiver = mpsc::UnboundedReceiver; +# +# fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +# where +# F: Future> + Send + 'static, +# { +# task::spawn(async move { +# if let Err(e) = fut.await { +# eprintln!("{}", e) +# } +# }) +# } +# +# +# async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result<()> { +# let stream = Arc::new(stream); // 2 +# let reader = BufReader::new(&*stream); +# let mut lines = reader.lines(); +# +# let name = match lines.next().await { +# None => Err("peer disconnected immediately")?, +# Some(line) => line?, +# }; +# broker.send(Event::NewPeer { name: name.clone(), stream: Arc::clone(&stream) }).await // 3 +# .unwrap(); +# +# while let Some(line) = lines.next().await { +# let line = line?; +# let (dest, msg) = match line.find(':') { +# None => continue, +# Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), +# }; +# let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); +# let msg: String = msg.trim().to_string(); +# +# broker.send(Event::Message { // 4 +# from: name.clone(), +# to: dest, +# msg, +# }).await.unwrap(); +# } +# Ok(()) +# } +# +# async fn connection_writer_loop( +# mut messages: Receiver, +# stream: Arc, +# ) -> Result<()> { +# let mut stream = &*stream; +# while let Some(msg) = messages.next().await { +# stream.write_all(msg.as_bytes()).await?; +# } +# Ok(()) +# } +# +# #[derive(Debug)] +# enum Event { +# NewPeer { +# name: String, +# stream: Arc, +# }, +# Message { +# from: String, +# to: Vec, +# msg: String, +# }, +# } +# +# async fn broker_loop(mut events: Receiver) -> Result<()> { +# let mut peers: HashMap> = HashMap::new(); +# +# while let Some(event) = events.next().await { +# match event { +# Event::Message { from, to, msg } => { +# for addr in to { +# if let Some(peer) = peers.get_mut(&addr) { +# let msg = format!("from {}: {}\n", from, msg); +# peer.send(msg).await? +# } +# } +# } +# Event::NewPeer { name, stream} => { +# match peers.entry(name) { +# Entry::Occupied(..) => (), +# Entry::Vacant(entry) => { +# let (client_sender, client_receiver) = mpsc::unbounded(); +# entry.insert(client_sender); // 4 +# spawn_and_log_error(connection_writer_loop(client_receiver, stream)); // 5 +# } +# } +# } +# } +# } +# Ok(()) +# } +# +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + let listener = TcpListener::bind(addr).await?; + + let (broker_sender, broker_receiver) = mpsc::unbounded(); + let broker_handle = task::spawn(broker_loop(broker_receiver)); + let mut incoming = listener.incoming(); + while let Some(stream) = incoming.next().await { + let stream = stream?; + println!("Accepting from: {}", stream.peer_addr()?); + spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); + } + drop(broker_sender); // 1 + broker_handle.await?; // 5 + Ok(()) +} +``` + +And to the broker: + +```rust,edition2018 +# extern crate async_std; +# extern crate futures; +# use async_std::{ +# io::{self, BufReader}, +# net::{TcpListener, TcpStream, ToSocketAddrs}, +# prelude::*, +# task, +# }; +# use futures::channel::mpsc; +# use futures::sink::SinkExt; +# use std::{ +# collections::hash_map::{HashMap, Entry}, +# sync::Arc, +# }; +# +# type Result = std::result::Result>; +# type Sender = mpsc::UnboundedSender; +# type Receiver = mpsc::UnboundedReceiver; +# +# async fn connection_writer_loop( +# mut messages: Receiver, +# stream: Arc, +# ) -> Result<()> { +# let mut stream = &*stream; +# while let Some(msg) = messages.next().await { +# stream.write_all(msg.as_bytes()).await?; +# } +# Ok(()) +# } +# +# fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +# where +# F: Future> + Send + 'static, +# { +# task::spawn(async move { +# if let Err(e) = fut.await { +# eprintln!("{}", e) +# } +# }) +# } +# +# #[derive(Debug)] +# enum Event { +# NewPeer { +# name: String, +# stream: Arc, +# }, +# Message { +# from: String, +# to: Vec, +# msg: String, +# }, +# } +# +async fn broker_loop(mut events: Receiver) -> Result<()> { + let mut writers = Vec::new(); + let mut peers: HashMap> = HashMap::new(); + while let Some(event) = events.next().await { // 2 + match event { + Event::Message { from, to, msg } => { + for addr in to { + if let Some(peer) = peers.get_mut(&addr) { + let msg = format!("from {}: {}\n", from, msg); + peer.send(msg).await? + } + } + } + Event::NewPeer { name, stream} => { + match peers.entry(name) { + Entry::Occupied(..) => (), + Entry::Vacant(entry) => { + let (client_sender, client_receiver) = mpsc::unbounded(); + entry.insert(client_sender); + let handle = spawn_and_log_error(connection_writer_loop(client_receiver, stream)); + writers.push(handle); // 4 + } + } + } + } + } + drop(peers); // 3 + for writer in writers { // 4 + writer.await; + } + Ok(()) +} +``` + +Notice what happens with all of the channels once we exit the accept loop: + +1. First, we drop the main broker's sender. + That way when the readers are done, there's no sender for the broker's channel, and the channel closes. +2. Next, the broker exits `while let Some(event) = events.next().await` loop. +3. It's crucial that, at this stage, we drop the `peers` map. + This drops writer's senders. +4. Now we can join all of the writers. +5. Finally, we join the broker, which also guarantees that all the writes have terminated. diff --git a/exercise-solutions/async-chat/book/src/tutorial/connecting_readers_and_writers.md b/exercise-solutions/async-chat/book/src/tutorial/connecting_readers_and_writers.md new file mode 100644 index 00000000..921cf90c --- /dev/null +++ b/exercise-solutions/async-chat/book/src/tutorial/connecting_readers_and_writers.md @@ -0,0 +1,100 @@ + +## Connecting Readers and Writers + +So how do we make sure that messages read in `connection_loop` flow into the relevant `connection_writer_loop`? +We should somehow maintain a `peers: HashMap>` map which allows a client to find destination channels. +However, this map would be a bit of shared mutable state, so we'll have to wrap an `RwLock` over it and answer tough questions of what should happen if the client joins at the same moment as it receives a message. + +One trick to make reasoning about state simpler comes from the actor model. +We can create a dedicated broker task which owns the `peers` map and communicates with other tasks using channels. +By hiding `peers` inside such an "actor" task, we remove the need for mutexes and also make the serialization point explicit. +The order of events "Bob sends message to Alice" and "Alice joins" is determined by the order of the corresponding events in the broker's event queue. + +```rust,edition2018 +# extern crate async_std; +# extern crate futures; +# use async_std::{ +# net::TcpStream, +# prelude::*, +# task, +# }; +# use futures::channel::mpsc; +# use futures::sink::SinkExt; +# use std::sync::Arc; +# +# type Result = std::result::Result>; +# type Sender = mpsc::UnboundedSender; +# type Receiver = mpsc::UnboundedReceiver; +# +# async fn connection_writer_loop( +# mut messages: Receiver, +# stream: Arc, +# ) -> Result<()> { +# let mut stream = &*stream; +# while let Some(msg) = messages.next().await { +# stream.write_all(msg.as_bytes()).await?; +# } +# Ok(()) +# } +# +# fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +# where +# F: Future> + Send + 'static, +# { +# task::spawn(async move { +# if let Err(e) = fut.await { +# eprintln!("{}", e) +# } +# }) +# } +# +use std::collections::hash_map::{Entry, HashMap}; + +#[derive(Debug)] +enum Event { // 1 + NewPeer { + name: String, + stream: Arc, + }, + Message { + from: String, + to: Vec, + msg: String, + }, +} + +async fn broker_loop(mut events: Receiver) -> Result<()> { + let mut peers: HashMap> = HashMap::new(); // 2 + + while let Some(event) = events.next().await { + match event { + Event::Message { from, to, msg } => { // 3 + for addr in to { + if let Some(peer) = peers.get_mut(&addr) { + let msg = format!("from {}: {}\n", from, msg); + peer.send(msg).await? + } + } + } + Event::NewPeer { name, stream } => { + match peers.entry(name) { + Entry::Occupied(..) => (), + Entry::Vacant(entry) => { + let (client_sender, client_receiver) = mpsc::unbounded(); + entry.insert(client_sender); // 4 + spawn_and_log_error(connection_writer_loop(client_receiver, stream)); // 5 + } + } + } + } + } + Ok(()) +} +``` + +1. The broker task should handle two types of events: a message or an arrival of a new peer. +2. The internal state of the broker is a `HashMap`. + Note how we don't need a `Mutex` here and can confidently say, at each iteration of the broker's loop, what is the current set of peers +3. To handle a message, we send it over a channel to each destination +4. To handle a new peer, we first register it in the peer's map ... +5. ... and then spawn a dedicated task to actually write the messages to the socket. diff --git a/exercise-solutions/async-chat/book/src/tutorial/handling_disconnection.md b/exercise-solutions/async-chat/book/src/tutorial/handling_disconnection.md new file mode 100644 index 00000000..b6e53641 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/tutorial/handling_disconnection.md @@ -0,0 +1,308 @@ +## Handling Disconnections + +Currently, we only ever _add_ new peers to the map. +This is clearly wrong: if a peer closes connection to the chat, we should not try to send any more messages to it. + +One subtlety with handling disconnection is that we can detect it either in the reader's task, or in the writer's task. +The most obvious solution here is to just remove the peer from the `peers` map in both cases, but this would be wrong. +If _both_ read and write fail, we'll remove the peer twice, but it can be the case that the peer reconnected between the two failures! +To fix this, we will only remove the peer when the write side finishes. +If the read side finishes we will notify the write side that it should stop as well. +That is, we need to add an ability to signal shutdown for the writer task. + +One way to approach this is a `shutdown: Receiver<()>` channel. +There's a more minimal solution however, which makes clever use of RAII. +Closing a channel is a synchronization event, so we don't need to send a shutdown message, we can just drop the sender. +This way, we statically guarantee that we issue shutdown exactly once, even if we early return via `?` or panic. + +First, let's add a shutdown channel to the `connection_loop`: + +```rust,edition2018 +# extern crate async_std; +# extern crate futures; +# use async_std::net::TcpStream; +# use futures::channel::mpsc; +# use futures::sink::SinkExt; +# use std::sync::Arc; +# +# type Result = std::result::Result>; +# type Sender = mpsc::UnboundedSender; +# type Receiver = mpsc::UnboundedReceiver; +# +#[derive(Debug)] +enum Void {} // 1 + +#[derive(Debug)] +enum Event { + NewPeer { + name: String, + stream: Arc, + shutdown: Receiver, // 2 + }, + Message { + from: String, + to: Vec, + msg: String, + }, +} + +async fn connection_loop(mut broker: Sender, stream: Arc) -> Result<()> { + // ... +# let name: String = unimplemented!(); + let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded::(); // 3 + broker.send(Event::NewPeer { + name: name.clone(), + stream: Arc::clone(&stream), + shutdown: shutdown_receiver, + }).await.unwrap(); + // ... +# unimplemented!() +} +``` + +1. To enforce that no messages are sent along the shutdown channel, we use an uninhabited type. +2. We pass the shutdown channel to the writer task. +3. In the reader, we create a `_shutdown_sender` whose only purpose is to get dropped. + +In the `connection_writer_loop`, we now need to choose between shutdown and message channels. +We use the `select` macro for this purpose: + +```rust,edition2018 +# extern crate async_std; +# extern crate futures; +# use async_std::{net::TcpStream, prelude::*}; +# use futures::channel::mpsc; +use futures::{select, FutureExt}; +# use std::sync::Arc; +# type Receiver = mpsc::UnboundedReceiver; +# type Result = std::result::Result>; +# type Sender = mpsc::UnboundedSender; +# #[derive(Debug)] +# enum Void {} // 1 + +async fn connection_writer_loop( + messages: &mut Receiver, + stream: Arc, + shutdown: Receiver, // 1 +) -> Result<()> { + let mut stream = &*stream; + let mut messages = messages.fuse(); + let mut shutdown = shutdown.fuse(); + loop { // 2 + select! { + msg = messages.next().fuse() => match msg { // 3 + Some(msg) => stream.write_all(msg.as_bytes()).await?, + None => break, + }, + void = shutdown.next().fuse() => match void { + Some(void) => match void {}, // 4 + None => break, + } + } + } + Ok(()) +} +``` + +1. We add shutdown channel as an argument. +2. Because of `select`, we can't use a `while let` loop, so we desugar it further into a `loop`. +3. Function fuse() is used to turn any `Stream` into a `FusedStream`. This is used for fusing a stream such that poll_next will never again be called once it has finished. +4. In the shutdown case we use `match void {}` as a statically-checked `unreachable!()`. + +Another problem is that between the moment we detect disconnection in `connection_writer_loop` and the moment when we actually remove the peer from the `peers` map, new messages might be pushed into the peer's channel. +To not lose these messages completely, we'll return the messages channel back to the broker. +This also allows us to establish a useful invariant that the message channel strictly outlives the peer in the `peers` map, and makes the broker itself infallible. + +## Final Code + +The final code looks like this: + +```rust,edition2018 +# extern crate async_std; +# extern crate futures; +use async_std::{ + io::BufReader, + net::{TcpListener, TcpStream, ToSocketAddrs}, + prelude::*, + task, +}; +use futures::channel::mpsc; +use futures::sink::SinkExt; +use futures::{select, FutureExt}; +use std::{ + collections::hash_map::{Entry, HashMap}, + future::Future, + sync::Arc, +}; + +type Result = std::result::Result>; +type Sender = mpsc::UnboundedSender; +type Receiver = mpsc::UnboundedReceiver; + +#[derive(Debug)] +enum Void {} + +// main +fn run() -> Result<()> { + task::block_on(accept_loop("127.0.0.1:8080")) +} + +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + let listener = TcpListener::bind(addr).await?; + let (broker_sender, broker_receiver) = mpsc::unbounded(); + let broker_handle = task::spawn(broker_loop(broker_receiver)); + let mut incoming = listener.incoming(); + while let Some(stream) = incoming.next().await { + let stream = stream?; + println!("Accepting from: {}", stream.peer_addr()?); + spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); + } + drop(broker_sender); + broker_handle.await; + Ok(()) +} + +async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result<()> { + let stream = Arc::new(stream); + let reader = BufReader::new(&*stream); + let mut lines = reader.lines(); + + let name = match lines.next().await { + None => Err("peer disconnected immediately")?, + Some(line) => line?, + }; + let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded::(); + broker.send(Event::NewPeer { + name: name.clone(), + stream: Arc::clone(&stream), + shutdown: shutdown_receiver, + }).await.unwrap(); + + while let Some(line) = lines.next().await { + let line = line?; + let (dest, msg) = match line.find(':') { + None => continue, + Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), + }; + let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); + let msg: String = msg.trim().to_string(); + + broker.send(Event::Message { + from: name.clone(), + to: dest, + msg, + }).await.unwrap(); + } + + Ok(()) +} + +async fn connection_writer_loop( + messages: &mut Receiver, + stream: Arc, + shutdown: Receiver, +) -> Result<()> { + let mut stream = &*stream; + let mut messages = messages.fuse(); + let mut shutdown = shutdown.fuse(); + loop { + select! { + msg = messages.next().fuse() => match msg { + Some(msg) => stream.write_all(msg.as_bytes()).await?, + None => break, + }, + void = shutdown.next().fuse() => match void { + Some(void) => match void {}, + None => break, + } + } + } + Ok(()) +} + +#[derive(Debug)] +enum Event { + NewPeer { + name: String, + stream: Arc, + shutdown: Receiver, + }, + Message { + from: String, + to: Vec, + msg: String, + }, +} + +async fn broker_loop(events: Receiver) { + let (disconnect_sender, mut disconnect_receiver) = // 1 + mpsc::unbounded::<(String, Receiver)>(); + let mut peers: HashMap> = HashMap::new(); + let mut events = events.fuse(); + loop { + let event = select! { + event = events.next().fuse() => match event { + None => break, // 2 + Some(event) => event, + }, + disconnect = disconnect_receiver.next().fuse() => { + let (name, _pending_messages) = disconnect.unwrap(); // 3 + assert!(peers.remove(&name).is_some()); + continue; + }, + }; + match event { + Event::Message { from, to, msg } => { + for addr in to { + if let Some(peer) = peers.get_mut(&addr) { + let msg = format!("from {}: {}\n", from, msg); + peer.send(msg).await + .unwrap() // 6 + } + } + } + Event::NewPeer { name, stream, shutdown } => { + match peers.entry(name.clone()) { + Entry::Occupied(..) => (), + Entry::Vacant(entry) => { + let (client_sender, mut client_receiver) = mpsc::unbounded(); + entry.insert(client_sender); + let mut disconnect_sender = disconnect_sender.clone(); + spawn_and_log_error(async move { + let res = connection_writer_loop(&mut client_receiver, stream, shutdown).await; + disconnect_sender.send((name, client_receiver)).await // 4 + .unwrap(); + res + }); + } + } + } + } + } + drop(peers); // 5 + drop(disconnect_sender); // 6 + while let Some((_name, _pending_messages)) = disconnect_receiver.next().await { + } +} + +fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +where + F: Future> + Send + 'static, +{ + task::spawn(async move { + if let Err(e) = fut.await { + eprintln!("{}", e) + } + }) +} +``` + +1. In the broker, we create a channel to reap disconnected peers and their undelivered messages. +2. The broker's main loop exits when the input events channel is exhausted (that is, when all readers exit). +3. Because broker itself holds a `disconnect_sender`, we know that the disconnections channel can't be fully drained in the main loop. +4. We send peer's name and pending messages to the disconnections channel in both the happy and the not-so-happy path. + Again, we can safely unwrap because the broker outlives writers. +5. We drop `peers` map to close writers' messages channel and shut down the writers for sure. + It is not strictly necessary in the current setup, where the broker waits for readers' shutdown anyway. + However, if we add a server-initiated shutdown (for example, kbd:[ctrl+c] handling), this will be a way for the broker to shutdown the writers. +6. Finally, we close and drain the disconnections channel. diff --git a/exercise-solutions/async-chat/book/src/tutorial/implementing_a_client.md b/exercise-solutions/async-chat/book/src/tutorial/implementing_a_client.md new file mode 100644 index 00000000..ba9d6f33 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/tutorial/implementing_a_client.md @@ -0,0 +1,63 @@ +## Implementing a client + +Since the protocol is line-based, implementing a client for the chat is straightforward: + +* Lines read from stdin should be sent over the socket. +* Lines read from the socket should be echoed to stdout. + +Although async does not significantly affect client performance (as unlike the server, the client interacts solely with one user and only needs limited concurrency), async is still useful for managing concurrency! + +The client has to read from stdin and the socket *simultaneously*. +Programming this with threads is cumbersome, especially when implementing a clean shutdown. +With async, the `select!` macro is all that is needed. + + +```rust,edition2018 +# extern crate async_std; +# extern crate futures; +use async_std::{ + io::{stdin, BufReader}, + net::{TcpStream, ToSocketAddrs}, + prelude::*, + task, +}; +use futures::{select, FutureExt}; + +type Result = std::result::Result>; + +// main +fn run() -> Result<()> { + task::block_on(try_run("127.0.0.1:8080")) +} + +async fn try_run(addr: impl ToSocketAddrs) -> Result<()> { + let stream = TcpStream::connect(addr).await?; + let (reader, mut writer) = (&stream, &stream); // 1 + let mut lines_from_server = BufReader::new(reader).lines().fuse(); // 2 + let mut lines_from_stdin = BufReader::new(stdin()).lines().fuse(); // 2 + loop { + select! { // 3 + line = lines_from_server.next().fuse() => match line { + Some(line) => { + let line = line?; + println!("{}", line); + }, + None => break, + }, + line = lines_from_stdin.next().fuse() => match line { + Some(line) => { + let line = line?; + writer.write_all(line.as_bytes()).await?; + writer.write_all(b"\n").await?; + } + None => break, + } + } + } + Ok(()) +} +``` + +1. Here we split `TcpStream` into read and write halves: there's `impl AsyncRead for &'_ TcpStream`, just like the one in std. +2. We create a stream of lines for both the socket and stdin. +3. In the main select loop, we print the lines we receive from the server and send the lines we read from the console. diff --git a/exercise-solutions/async-chat/book/src/tutorial/index.md b/exercise-solutions/async-chat/book/src/tutorial/index.md new file mode 100644 index 00000000..076ecf41 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/tutorial/index.md @@ -0,0 +1,14 @@ +# Tutorial: Writing a chat + +Nothing is simpler than creating a chat server, right? +Not quite, chat servers expose you to all the fun of asynchronous programming: + +How will the server handle clients connecting concurrently? + +How will it handle them disconnecting? + +How will it distribute the messages? + +This tutorial explains how to write a chat server in `async-std`. + +You can also find the tutorial in [our repository](https://github.com/async-rs/async-std/blob/HEAD/examples/a-chat). diff --git a/exercise-solutions/async-chat/book/src/tutorial/receiving_messages.md b/exercise-solutions/async-chat/book/src/tutorial/receiving_messages.md new file mode 100644 index 00000000..036bc459 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/tutorial/receiving_messages.md @@ -0,0 +1,145 @@ +## Receiving messages + +Let's implement the receiving part of the protocol. +We need to: + +1. split incoming `TcpStream` on `\n` and decode bytes as utf-8 +2. interpret the first line as a login +3. parse the rest of the lines as a `login: message` + +```rust,edition2018 +# extern crate async_std; +# use async_std::{ +# net::{TcpListener, ToSocketAddrs}, +# prelude::*, +# task, +# }; +# +# type Result = std::result::Result>; +# +use async_std::{ + io::BufReader, + net::TcpStream, +}; + +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + let listener = TcpListener::bind(addr).await?; + let mut incoming = listener.incoming(); + while let Some(stream) = incoming.next().await { + let stream = stream?; + println!("Accepting from: {}", stream.peer_addr()?); + let _handle = task::spawn(connection_loop(stream)); // 1 + } + Ok(()) +} + +async fn connection_loop(stream: TcpStream) -> Result<()> { + let reader = BufReader::new(&stream); // 2 + let mut lines = reader.lines(); + + let name = match lines.next().await { // 3 + None => Err("peer disconnected immediately")?, + Some(line) => line?, + }; + println!("name = {}", name); + + while let Some(line) = lines.next().await { // 4 + let line = line?; + let (dest, msg) = match line.find(':') { // 5 + None => continue, + Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), + }; + let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); + let msg: String = msg.to_string(); + } + Ok(()) +} +``` + +1. We use `task::spawn` function to spawn an independent task for working with each client. + That is, after accepting the client the `accept_loop` immediately starts waiting for the next one. + This is the core benefit of event-driven architecture: we serve many clients concurrently, without spending many hardware threads. + +2. Luckily, the "split byte stream into lines" functionality is already implemented. + `.lines()` call returns a stream of `String`'s. + +3. We get the first line -- login + +4. And, once again, we implement a manual async for loop. + +5. Finally, we parse each line into a list of destination logins and the message itself. + +## Managing Errors + +One serious problem in the above solution is that, while we correctly propagate errors in the `connection_loop`, we just drop the error on the floor afterwards! +That is, `task::spawn` does not return an error immediately (it can't, it needs to run the future to completion first), only after it is joined. +We can "fix" it by waiting for the task to be joined, like this: + +```rust,edition2018 +# #![feature(async_closure)] +# extern crate async_std; +# use async_std::{ +# io::BufReader, +# net::{TcpListener, TcpStream, ToSocketAddrs}, +# prelude::*, +# task, +# }; +# +# type Result = std::result::Result>; +# +# async fn connection_loop(stream: TcpStream) -> Result<()> { +# let reader = BufReader::new(&stream); // 2 +# let mut lines = reader.lines(); +# +# let name = match lines.next().await { // 3 +# None => Err("peer disconnected immediately")?, +# Some(line) => line?, +# }; +# println!("name = {}", name); +# +# while let Some(line) = lines.next().await { // 4 +# let line = line?; +# let (dest, msg) = match line.find(':') { // 5 +# None => continue, +# Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), +# }; +# let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); +# let msg: String = msg.trim().to_string(); +# } +# Ok(()) +# } +# +# async move |stream| { +let handle = task::spawn(connection_loop(stream)); +handle.await? +# }; +``` + +The `.await` waits until the client finishes, and `?` propagates the result. + +There are two problems with this solution however! +*First*, because we immediately await the client, we can only handle one client at a time, and that completely defeats the purpose of async! +*Second*, if a client encounters an IO error, the whole server immediately exits. +That is, a flaky internet connection of one peer brings down the whole chat room! + +A correct way to handle client errors in this case is log them, and continue serving other clients. +So let's use a helper function for this: + +```rust,edition2018 +# extern crate async_std; +# use async_std::{ +# io, +# prelude::*, +# task, +# }; +fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +where + F: Future> + Send + 'static, +{ + task::spawn(async move { + if let Err(e) = fut.await { + eprintln!("{}", e) + } + }) +} +``` diff --git a/exercise-solutions/async-chat/book/src/tutorial/sending_messages.md b/exercise-solutions/async-chat/book/src/tutorial/sending_messages.md new file mode 100644 index 00000000..3f426d02 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/tutorial/sending_messages.md @@ -0,0 +1,44 @@ +## Sending Messages + +Now it's time to implement the other half -- sending messages. +A most obvious way to implement sending is to give each `connection_loop` access to the write half of `TcpStream` of each other clients. +That way, a client can directly `.write_all` a message to recipients. +However, this would be wrong: if Alice sends `bob: foo`, and Charley sends `bob: bar`, Bob might actually receive `fobaor`. +Sending a message over a socket might require several syscalls, so two concurrent `.write_all`'s might interfere with each other! + +As a rule of thumb, only a single task should write to each `TcpStream`. +So let's create a `connection_writer_loop` task which receives messages over a channel and writes them to the socket. +This task would be the point of serialization of messages. +if Alice and Charley send two messages to Bob at the same time, Bob will see the messages in the same order as they arrive in the channel. + +```rust,edition2018 +# extern crate async_std; +# extern crate futures; +# use async_std::{ +# net::TcpStream, +# prelude::*, +# }; +use futures::channel::mpsc; // 1 +use futures::sink::SinkExt; +use std::sync::Arc; + +# type Result = std::result::Result>; +type Sender = mpsc::UnboundedSender; // 2 +type Receiver = mpsc::UnboundedReceiver; + +async fn connection_writer_loop( + mut messages: Receiver, + stream: Arc, // 3 +) -> Result<()> { + let mut stream = &*stream; + while let Some(msg) = messages.next().await { + stream.write_all(msg.as_bytes()).await?; + } + Ok(()) +} +``` + +1. We will use channels from the `futures` crate. +2. For simplicity, we will use `unbounded` channels, and won't be discussing backpressure in this tutorial. +3. As `connection_loop` and `connection_writer_loop` share the same `TcpStream`, we need to put it into an `Arc`. + Note that because `client` only reads from the stream and `connection_writer_loop` only writes to the stream, we don't get a race here. diff --git a/exercise-solutions/async-chat/book/src/tutorial/specification.md b/exercise-solutions/async-chat/book/src/tutorial/specification.md new file mode 100644 index 00000000..7b1a0167 --- /dev/null +++ b/exercise-solutions/async-chat/book/src/tutorial/specification.md @@ -0,0 +1,47 @@ +# Specification and Getting Started + +## Specification + +The chat uses a simple text protocol over TCP. +The protocol consists of utf-8 messages, separated by `\n`. + +The client connects to the server and sends login as a first line. +After that, the client can send messages to other clients using the following syntax: + +```text +login1, login2, ... loginN: message +``` + +Each of the specified clients then receives a `from login: message` message. + +A possible session might look like this + +```text +On Alice's computer: | On Bob's computer: + +> alice | > bob +> bob: hello < from alice: hello + | > alice, bob: hi! + < from bob: hi! +< from bob: hi! | +``` + +The main challenge for the chat server is keeping track of many concurrent connections. +The main challenge for the chat client is managing concurrent outgoing messages, incoming messages and user's typing. + +## Getting Started + +Let's create a new Cargo project: + +```bash +$ cargo new a-chat +$ cd a-chat +``` + +Add the following lines to `Cargo.toml`: + +```toml +[dependencies] +futures = "0.3.0" +async-std = "1" +``` From 07f2b3411bb5561af3b48643404b084532c40330 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 10:23:09 +0200 Subject: [PATCH 10/36] Remove everything except the tutorial --- .../async-chat/{book => }/.gitignore | 0 exercise-solutions/async-chat/book.toml | 6 + exercise-solutions/async-chat/book/book.toml | 13 - .../async-chat/book/src/SUMMARY.md | 26 -- .../async-chat/book/src/concepts.md | 15 - .../book/src/concepts/async-read-write.md | 1 - .../async-chat/book/src/concepts/data.csv | 0 .../async-chat/book/src/concepts/futures.md | 143 ---------- .../async-chat/book/src/concepts/streams.md | 1 - .../async-chat/book/src/concepts/tasks.md | 150 ---------- .../async-chat/book/src/glossary.md | 7 - .../async-chat/book/src/images/async_1.svg | 165 ----------- .../async-chat/book/src/images/async_2.svg | 66 ----- .../book/src/images/horizontal_color.svg | 78 ----- .../async-chat/book/src/images/icon_color.svg | 80 ------ .../book/src/images/vertical_color.svg | 85 ------ .../async-chat/book/src/introduction.md | 9 - .../async-chat/book/src/overview/async-std.md | 7 - .../book/src/overview/stability-guarantees.md | 40 --- .../async-chat/book/src/patterns.md | 5 - .../book/src/patterns/accept-loop.md | 266 ------------------ .../patterns/accepting-concurrent-requests.md | 1 - .../book/src/patterns/async-read-write.md | 1 - .../book/src/patterns/background-tasks.md | 1 - .../async-chat/book/src/patterns/fork-join.md | 1 - .../book/src/patterns/proper-shutdown.md | 1 - .../book/src/patterns/small-patterns.md | 16 -- .../async-chat/book/src/patterns/testing.md | 1 - .../async-chat/book/src/security/index.md | 12 - .../async-chat/book/src/security/policy.md | 66 ----- exercise-solutions/async-chat/docs/SUMMARY.md | 12 + .../src/tutorial => docs}/accept_loop.md | 0 .../src/tutorial => docs}/all_together.md | 0 .../src/tutorial => docs}/clean_shutdown.md | 0 .../connecting_readers_and_writers.md | 0 .../handling_disconnection.md | 0 .../implementing_a_client.md | 0 .../{book/src/tutorial => docs}/index.md | 0 .../tutorial => docs}/receiving_messages.md | 0 .../src/tutorial => docs}/sending_messages.md | 0 .../src/tutorial => docs}/specification.md | 0 41 files changed, 18 insertions(+), 1257 deletions(-) rename exercise-solutions/async-chat/{book => }/.gitignore (100%) create mode 100644 exercise-solutions/async-chat/book.toml delete mode 100644 exercise-solutions/async-chat/book/book.toml delete mode 100644 exercise-solutions/async-chat/book/src/SUMMARY.md delete mode 100644 exercise-solutions/async-chat/book/src/concepts.md delete mode 100644 exercise-solutions/async-chat/book/src/concepts/async-read-write.md delete mode 100644 exercise-solutions/async-chat/book/src/concepts/data.csv delete mode 100644 exercise-solutions/async-chat/book/src/concepts/futures.md delete mode 100644 exercise-solutions/async-chat/book/src/concepts/streams.md delete mode 100644 exercise-solutions/async-chat/book/src/concepts/tasks.md delete mode 100644 exercise-solutions/async-chat/book/src/glossary.md delete mode 100644 exercise-solutions/async-chat/book/src/images/async_1.svg delete mode 100644 exercise-solutions/async-chat/book/src/images/async_2.svg delete mode 100644 exercise-solutions/async-chat/book/src/images/horizontal_color.svg delete mode 100644 exercise-solutions/async-chat/book/src/images/icon_color.svg delete mode 100644 exercise-solutions/async-chat/book/src/images/vertical_color.svg delete mode 100644 exercise-solutions/async-chat/book/src/introduction.md delete mode 100644 exercise-solutions/async-chat/book/src/overview/async-std.md delete mode 100644 exercise-solutions/async-chat/book/src/overview/stability-guarantees.md delete mode 100644 exercise-solutions/async-chat/book/src/patterns.md delete mode 100644 exercise-solutions/async-chat/book/src/patterns/accept-loop.md delete mode 100644 exercise-solutions/async-chat/book/src/patterns/accepting-concurrent-requests.md delete mode 100644 exercise-solutions/async-chat/book/src/patterns/async-read-write.md delete mode 100644 exercise-solutions/async-chat/book/src/patterns/background-tasks.md delete mode 100644 exercise-solutions/async-chat/book/src/patterns/fork-join.md delete mode 100644 exercise-solutions/async-chat/book/src/patterns/proper-shutdown.md delete mode 100644 exercise-solutions/async-chat/book/src/patterns/small-patterns.md delete mode 100644 exercise-solutions/async-chat/book/src/patterns/testing.md delete mode 100644 exercise-solutions/async-chat/book/src/security/index.md delete mode 100644 exercise-solutions/async-chat/book/src/security/policy.md create mode 100644 exercise-solutions/async-chat/docs/SUMMARY.md rename exercise-solutions/async-chat/{book/src/tutorial => docs}/accept_loop.md (100%) rename exercise-solutions/async-chat/{book/src/tutorial => docs}/all_together.md (100%) rename exercise-solutions/async-chat/{book/src/tutorial => docs}/clean_shutdown.md (100%) rename exercise-solutions/async-chat/{book/src/tutorial => docs}/connecting_readers_and_writers.md (100%) rename exercise-solutions/async-chat/{book/src/tutorial => docs}/handling_disconnection.md (100%) rename exercise-solutions/async-chat/{book/src/tutorial => docs}/implementing_a_client.md (100%) rename exercise-solutions/async-chat/{book/src/tutorial => docs}/index.md (100%) rename exercise-solutions/async-chat/{book/src/tutorial => docs}/receiving_messages.md (100%) rename exercise-solutions/async-chat/{book/src/tutorial => docs}/sending_messages.md (100%) rename exercise-solutions/async-chat/{book/src/tutorial => docs}/specification.md (100%) diff --git a/exercise-solutions/async-chat/book/.gitignore b/exercise-solutions/async-chat/.gitignore similarity index 100% rename from exercise-solutions/async-chat/book/.gitignore rename to exercise-solutions/async-chat/.gitignore diff --git a/exercise-solutions/async-chat/book.toml b/exercise-solutions/async-chat/book.toml new file mode 100644 index 00000000..455a37f7 --- /dev/null +++ b/exercise-solutions/async-chat/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Ferrous Systems GmbH"] +language = "en" +multilingual = false +src = "docs" +title = "Async programming tutorial in Rust with tokio" diff --git a/exercise-solutions/async-chat/book/book.toml b/exercise-solutions/async-chat/book/book.toml deleted file mode 100644 index 215f872f..00000000 --- a/exercise-solutions/async-chat/book/book.toml +++ /dev/null @@ -1,13 +0,0 @@ -[book] -authors = ["The async-std maintainers"] -language = "en" -multilingual = false -src = "src" -title = "Async programming in Rust with async-std" - -[build] -create-missing = false - -[output.html] -git-repository-url = "https://github.com/async-rs/async-std" -git-repository-icon = "fa-github" diff --git a/exercise-solutions/async-chat/book/src/SUMMARY.md b/exercise-solutions/async-chat/book/src/SUMMARY.md deleted file mode 100644 index 9e828f66..00000000 --- a/exercise-solutions/async-chat/book/src/SUMMARY.md +++ /dev/null @@ -1,26 +0,0 @@ -# Summary - -- [Introduction](./introduction.md) - - [Welcome to `async-std`!](./overview/async-std.md) - - [Stability guarantees](./overview/stability-guarantees.md) -- [Async concepts using async-std](./concepts.md) - - [Futures](./concepts/futures.md) - - [Tasks](./concepts/tasks.md) - - [Async read/write](./concepts/async-read-write.md) - - [Streams and Channels](./concepts/streams.md) -- [Tutorial: Implementing a chat](./tutorial/index.md) - - [Specification and Getting started](./tutorial/specification.md) - - [Writing an Accept Loop](./tutorial/accept_loop.md) - - [Receiving Messages](./tutorial/receiving_messages.md) - - [Sending Messages](./tutorial/sending_messages.md) - - [Connecting Readers and Writers](./tutorial/connecting_readers_and_writers.md) - - [All Together](./tutorial/all_together.md) - - [Clean Shutdown](./tutorial/clean_shutdown.md) - - [Handling Disconnection](./tutorial/handling_disconnection.md) - - [Implementing a Client](./tutorial/implementing_a_client.md) -- [Async Patterns](./patterns.md) - - [TODO: Collected Small Patterns](./patterns/small-patterns.md) - - [Production-Ready Accept Loop](./patterns/accept-loop.md) -- [Security practices](./security/index.md) - - [Security Disclosures and Policy](./security/policy.md) -- [Glossary](./glossary.md) diff --git a/exercise-solutions/async-chat/book/src/concepts.md b/exercise-solutions/async-chat/book/src/concepts.md deleted file mode 100644 index 8e25cb12..00000000 --- a/exercise-solutions/async-chat/book/src/concepts.md +++ /dev/null @@ -1,15 +0,0 @@ -# Async concepts using async-std - -[Rust Futures][futures] have the reputation of being hard. We don't think this is the case. They are, in our opinion, one of the easiest concurrency concepts around and have an intuitive explanation. - -However, there are good reasons for that perception. Futures have three concepts at their base that seem to be a constant source of confusion: deferred computation, asynchronicity and independence of execution strategy. - -These concepts are not hard, but something many people are not used to. This base confusion is amplified by many implementations oriented on details. Most explanations of these implementations also target advanced users, and can be hard for beginners. We try to provide both easy-to-understand primitives and approachable overviews of the concepts. - -Futures are a concept that abstracts over how code is run. By themselves, they do nothing. This is a weird concept in an imperative language, where usually one thing happens after the other - right now. - -So how do Futures run? You decide! Futures do nothing without the piece of code _executing_ them. This part is called an _executor_. An _executor_ decides _when_ and _how_ to execute your futures. The `async-std::task` module provides you with an interface to such an executor. - -Let's start with a little bit of motivation, though. - -[futures]: https://en.wikipedia.org/wiki/Futures_and_promises diff --git a/exercise-solutions/async-chat/book/src/concepts/async-read-write.md b/exercise-solutions/async-chat/book/src/concepts/async-read-write.md deleted file mode 100644 index 15675e99..00000000 --- a/exercise-solutions/async-chat/book/src/concepts/async-read-write.md +++ /dev/null @@ -1 +0,0 @@ -# TODO: Async read/write diff --git a/exercise-solutions/async-chat/book/src/concepts/data.csv b/exercise-solutions/async-chat/book/src/concepts/data.csv deleted file mode 100644 index e69de29b..00000000 diff --git a/exercise-solutions/async-chat/book/src/concepts/futures.md b/exercise-solutions/async-chat/book/src/concepts/futures.md deleted file mode 100644 index 7d9cc636..00000000 --- a/exercise-solutions/async-chat/book/src/concepts/futures.md +++ /dev/null @@ -1,143 +0,0 @@ -# Futures - -A notable point about Rust is [*fearless concurrency*](https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html). That is the notion that you should be empowered to do concurrent things, without giving up safety. Also, Rust being a low-level language, it's about fearless concurrency *without picking a specific implementation strategy*. This means we *must* abstract over the strategy, to allow choice *later*, if we want to have any way to share code between users of different strategies. - -Futures abstract over *computation*. They describe the "what", independent of the "where" and the "when". For that, they aim to break code into small, composable actions that can then be executed by a part of our system. Let's take a tour through what it means to compute things to find where we can abstract. - -## Send and Sync - -Luckily, concurrent Rust already has two well-known and effective concepts abstracting over sharing between concurrent parts of a program: `Send` and `Sync`. Notably, both the `Send` and `Sync` traits abstract over *strategies* of concurrent work, compose neatly, and don't prescribe an implementation. - -As a quick summary: - -- `Send` abstracts over *passing data* in a computation to another concurrent computation (let's call it the receiver), losing access to it on the sender side. In many programming languages, this strategy is commonly implemented, but missing support from the language side, and expects you to enforce the "losing access" behaviour yourself. This is a regular source of bugs: senders keeping handles to sent things around and maybe even working with them after sending. Rust mitigates this problem by making this behaviour known. Types can be `Send` or not (by implementing the appropriate marker trait), allowing or disallowing sending them around, and the ownership and borrowing rules prevent subsequent access. - -- `Sync` is about *sharing data* between two concurrent parts of a program. This is another common pattern: as writing to a memory location or reading while another party is writing is inherently unsafe, this access needs to be moderated through synchronisation.[^1] There are many common ways for two parties to agree on not using the same part in memory at the same time, for example mutexes and spinlocks. Again, Rust gives you the option of (safely!) not caring. Rust gives you the ability to express that something *needs* synchronisation while not being specific about the *how*. - -Note how we avoided any word like *"thread"*, but instead opted for "computation". The full power of `Send` and `Sync` is that they relieve you of the burden of knowing *what* shares. At the point of implementation, you only need to know which method of sharing is appropriate for the type at hand. This keeps reasoning local and is not influenced by whatever implementation the user of that type later uses. - -`Send` and `Sync` can be composed in interesting fashions, but that's beyond the scope here. You can find examples in the [Rust Book][rust-book-sync]. - -[rust-book-sync]: https://doc.rust-lang.org/stable/book/ch16-04-extensible-concurrency-sync-and-send.html - -To sum up: Rust gives us the ability to safely abstract over important properties of concurrent programs, their data sharing. It does so in a very lightweight fashion; the language itself only knows about the two markers `Send` and `Sync` and helps us a little by deriving them itself, when possible. The rest is a library concern. - -## An easy view of computation - -While computation is a subject to write a whole [book](https://computationbook.com/) about, a very simplified view suffices for us: A sequence of composable operations which can branch based on a decision, run to succession and yield a result or yield an error - -## Deferring computation - -As mentioned above, `Send` and `Sync` are about data. But programs are not only about data, they also talk about *computing* the data. And that's what [`Futures`][futures] do. We are going to have a close look at how that works in the next chapter. Let's look at what Futures allow us to express, in English. Futures go from this plan: - -- Do X -- If X succeeded, do Y - -towards: - -- Start doing X -- Once X succeeds, start doing Y - -Remember the talk about "deferred computation" in the intro? That's all it is. Instead of telling the computer what to execute and decide upon *now*, you tell it what to start doing and how to react on potential events in the... well... `Future`. - -[futures]: https://doc.rust-lang.org/std/future/trait.Future.html - -## Orienting towards the beginning - -Let's have a look at a simple function, specifically the return value: - -```rust,edition2018 -# use std::{fs::File, io, io::prelude::*}; -# -fn read_file(path: &str) -> io::Result { - let mut file = File::open(path)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - Ok(contents) -} -``` - -You can call that at any time, so you are in full control on when you call it. But here's the problem: the moment you call it, you transfer control to the called function until it returns a value - eventually. -Note that this return value talks about the past. The past has a drawback: all decisions have been made. It has an advantage: the outcome is visible. We can unwrap the results of the program's past computation, and then decide what to do with it. - -But we wanted to abstract over *computation* and let someone else choose how to run it. That's fundamentally incompatible with looking at the results of previous computation all the time. So, let's find a type that *describes* a computation without running it. Let's look at the function again: - -```rust,edition2018 -# use std::{fs::File, io, io::prelude::*}; -# -fn read_file(path: &str) -> io::Result { - let mut file = File::open(path)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - Ok(contents) -} -``` - -Speaking in terms of time, we can only take action *before* calling the function or *after* the function returned. This is not desirable, as it takes from us the ability to do something *while* it runs. When working with parallel code, this would take from us the ability to start a parallel task while the first runs (because we gave away control). - -This is the moment where we could reach for [threads](https://en.wikipedia.org/wiki/Thread_). But threads are a very specific concurrency primitive and we said that we are searching for an abstraction. - -What we are searching for is something that represents ongoing work towards a result in the future. Whenever we say "something" in Rust, we almost always mean a trait. Let's start with an incomplete definition of the `Future` trait: - -```rust,edition2018 -# use std::{pin::Pin, task::{Context, Poll}}; -# -trait Future { - type Output; - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll; -} -``` - -Looking at it closely, we see the following: - -- It is generic over the `Output`. -- It provides a function called `poll`, which allows us to check on the state of the current computation. -- (Ignore `Pin` and `Context` for now, you don't need them for high-level understanding.) - -Every call to `poll()` can result in one of these two cases: - -1. The computation is done, `poll` will return [`Poll::Ready`](https://doc.rust-lang.org/std/task/enum.Poll.html#variant.Ready) -2. The computation has not finished executing, it will return [`Poll::Pending`](https://doc.rust-lang.org/std/task/enum.Poll.html#variant.Pending) - -This allows us to externally check if a `Future` still has unfinished work, or is finally done and can give us the value. The most simple (but not efficient) way would be to just constantly poll futures in a loop. There are optimisations possible, and this is what a good runtime does for you. -Note that calling `poll` again after case 1 happened may result in confusing behaviour. See the [futures-docs](https://doc.rust-lang.org/std/future/trait.Future.html) for details. - -## Async - -While the `Future` trait has existed in Rust for a while, it was inconvenient to build and describe them. For this, Rust now has a special syntax: `async`. The example from above, implemented with `async-std`, would look like this: - -```rust,edition2018 -# extern crate async_std; -# use async_std::{fs::File, io, io::prelude::*}; -# -async fn read_file(path: &str) -> io::Result { - let mut file = File::open(path).await?; - let mut contents = String::new(); - file.read_to_string(&mut contents).await?; - Ok(contents) -} -``` - -Amazingly little difference, right? All we did is label the function `async` and insert 2 special commands: `.await`. - -This `async` function sets up a deferred computation. When this function is called, it will produce a `Future>` instead of immediately returning a `io::Result`. (Or, more precisely, generate a type for you that implements `Future>`.) - -## What does `.await` do? - -The `.await` postfix does exactly what it says on the tin: the moment you use it, the code will wait until the requested action (e.g. opening a file or reading all data in it) is finished. The `.await?` is not special, it's just the application of the `?` operator to the result of `.await`. So, what is gained over the initial code example? We're getting futures and then immediately waiting for them? - -The `.await` points act as a marker. Here, the code will wait for a `Future` to produce its value. How will a future finish? You don't need to care! The marker allows the component (usually called the “runtime”) in charge of *executing* this piece of code to take care of all the other things it has to do while the computation finishes. It will come back to this point when the operation you are doing in the background is done. This is why this style of programming is also called *evented programming*. We are waiting for *things to happen* (e.g. a file to be opened) and then react (by starting to read). - -When executing 2 or more of these functions at the same time, our runtime system is then able to fill the wait time with handling *all the other events* currently going on. - -## Conclusion - -Working from values, we searched for something that expresses *working towards a value available later*. From there, we talked about the concept of polling. - -A `Future` is any data type that does not represent a value, but the ability to *produce a value at some point in the future*. Implementations of this are very varied and detailed depending on use-case, but the interface is simple. - -Next, we will introduce you to `tasks`, which we will use to actually *run* Futures. - -[^1]: Two parties reading while it is guaranteed that no one is writing is always safe. - -[futures]: https://rust-lang.github.io/async-book/02_execution/02_future.html diff --git a/exercise-solutions/async-chat/book/src/concepts/streams.md b/exercise-solutions/async-chat/book/src/concepts/streams.md deleted file mode 100644 index 7f319c7c..00000000 --- a/exercise-solutions/async-chat/book/src/concepts/streams.md +++ /dev/null @@ -1 +0,0 @@ -# TODO: Streams diff --git a/exercise-solutions/async-chat/book/src/concepts/tasks.md b/exercise-solutions/async-chat/book/src/concepts/tasks.md deleted file mode 100644 index c3dbbe20..00000000 --- a/exercise-solutions/async-chat/book/src/concepts/tasks.md +++ /dev/null @@ -1,150 +0,0 @@ -# Tasks - -Now that we know what Futures are, we want to run them! - -In `async-std`, the [`task`][tasks] module is responsible for this. The simplest way is using the `block_on` function: - -```rust,edition2018 -# extern crate async_std; -use async_std::{fs::File, io, prelude::*, task}; - -async fn read_file(path: &str) -> io::Result { - let mut file = File::open(path).await?; - let mut contents = String::new(); - file.read_to_string(&mut contents).await?; - Ok(contents) -} - -fn main() { - let reader_task = task::spawn(async { - let result = read_file("data.csv").await; - match result { - Ok(s) => println!("{}", s), - Err(e) => println!("Error reading file: {:?}", e) - } - }); - println!("Started task!"); - task::block_on(reader_task); - println!("Stopped task!"); -} -``` - -This asks the runtime baked into `async_std` to execute the code that reads a file. Let's go one by one, though, inside to outside. - -```rust,edition2018 -# extern crate async_std; -# use async_std::{fs::File, io, prelude::*, task}; -# -# async fn read_file(path: &str) -> io::Result { -# let mut file = File::open(path).await?; -# let mut contents = String::new(); -# file.read_to_string(&mut contents).await?; -# Ok(contents) -# } -# -async { - let result = read_file("data.csv").await; - match result { - Ok(s) => println!("{}", s), - Err(e) => println!("Error reading file: {:?}", e) - } -}; -``` - -This is an `async` *block*. Async blocks are necessary to call `async` functions, and will instruct the compiler to include all the relevant instructions to do so. In Rust, all blocks return a value and `async` blocks happen to return a value of the kind `Future`. - -But let's get to the interesting part: - -```rust,edition2018 -# extern crate async_std; -# use async_std::task; -task::spawn(async { }); -``` - -`spawn` takes a `Future` and starts running it on a `Task`. It returns a `JoinHandle`. Futures in Rust are sometimes called *cold* Futures. You need something that starts running them. To run a Future, there may be some additional bookkeeping required, e.g. whether it's running or finished, where it is being placed in memory and what the current state is. This bookkeeping part is abstracted away in a `Task`. - -A `Task` is similar to a `Thread`, with some minor differences: it will be scheduled by the program instead of the operating system kernel, and if it encounters a point where it needs to wait, the program itself is responsible for waking it up again. We'll talk a little bit about that later. An `async_std` task can also have a name and an ID, just like a thread. - -For now, it is enough to know that once you have `spawn`ed a task, it will continue running in the background. The `JoinHandle` is itself a future that will finish once the `Task` has run to conclusion. Much like with `threads` and the `join` function, we can now call `block_on` on the handle to *block* the program (or the calling thread, to be specific) and wait for it to finish. - -## Tasks in `async_std` - -Tasks in `async_std` are one of the core abstractions. Much like Rust's `thread`s, they provide some practical functionality over the raw concept. `Tasks` have a relationship to the runtime, but they are in themselves separate. `async_std` tasks have a number of desirable properties: - -- They are allocated in one single allocation -- All tasks have a *backchannel*, which allows them to propagate results and errors to the spawning task through the `JoinHandle` -- They carry useful metadata for debugging -- They support task local storage - -`async_std`s task API handles setup and teardown of a backing runtime for you and doesn't rely on a runtime being explicitly started. - -## Blocking - -`Task`s are assumed to run _concurrently_, potentially by sharing a thread of execution. This means that operations blocking an _operating system thread_, such as `std::thread::sleep` or io function from Rust's `std` library will _stop execution of all tasks sharing this thread_. Other libraries (such as database drivers) have similar behaviour. Note that _blocking the current thread_ is not in and of itself bad behaviour, just something that does not mix well with the concurrent execution model of `async-std`. Essentially, never do this: - -```rust,edition2018 -# extern crate async_std; -# use async_std::task; -fn main() { - task::block_on(async { - // this is std::fs, which blocks - std::fs::read_to_string("test_file"); - }) -} -``` - -If you want to mix operation kinds, consider putting such blocking operations on a separate `thread`. - -## Errors and panics - -Tasks report errors through normal patterns: If they are fallible, their `Output` should be of kind `Result`. - -In case of `panic`, behaviour differs depending on whether there's a reasonable part that addresses the `panic`. If not, the program _aborts_. - -In practice, that means that `block_on` propagates panics to the blocking component: - -```rust,edition2018,should_panic -# extern crate async_std; -# use async_std::task; -fn main() { - task::block_on(async { - panic!("test"); - }); -} -``` - -```text -thread 'async-task-driver' panicked at 'test', examples/panic.rs:8:9 -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace. -``` - -While panicking a spawned task will abort: - -```rust,edition2018,should_panic -# extern crate async_std; -# use async_std::task; -# use std::time::Duration; -task::spawn(async { - panic!("test"); -}); - -task::block_on(async { - task::sleep(Duration::from_millis(10000)).await; -}) -``` - -```text -thread 'async-task-driver' panicked at 'test', examples/panic.rs:8:9 -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace. -Aborted (core dumped) -``` - -That might seem odd at first, but the other option would be to silently ignore panics in spawned tasks. The current behaviour can be changed by catching panics in the spawned task and reacting with custom behaviour. This gives users the choice of panic handling strategy. - -## Conclusion - -`async_std` comes with a useful `Task` type that works with an API similar to `std::thread`. It covers error and panic behaviour in a structured and defined way. - -Tasks are separate concurrent units and sometimes they need to communicate. That's where `Stream`s come in. - -[tasks]: https://docs.rs/async-std/latest/async_std/task/index.html diff --git a/exercise-solutions/async-chat/book/src/glossary.md b/exercise-solutions/async-chat/book/src/glossary.md deleted file mode 100644 index 42d6b4d9..00000000 --- a/exercise-solutions/async-chat/book/src/glossary.md +++ /dev/null @@ -1,7 +0,0 @@ -# Glossary - -### blocking - -"blocked" generally refers to conditions that keep a task from doing its work. For example, it might need data to be sent by a client before continuing. When tasks become blocked, usually, other tasks are scheduled. - -Sometimes you hear that you should never call "blocking functions" in an async context. What this refers to is functions that block the current thread and do not yield control back. This keeps the executor from using this thread to schedule another task. diff --git a/exercise-solutions/async-chat/book/src/images/async_1.svg b/exercise-solutions/async-chat/book/src/images/async_1.svg deleted file mode 100644 index be14812a..00000000 --- a/exercise-solutions/async-chat/book/src/images/async_1.svg +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - T - - - - - - T - - - - - - - - - - - - - - - - - - - - ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - T - - - - - - T - - - - - - T - - - - - - - T - - - - - - T - - - - - diff --git a/exercise-solutions/async-chat/book/src/images/async_2.svg b/exercise-solutions/async-chat/book/src/images/async_2.svg deleted file mode 100644 index fff4b3a9..00000000 --- a/exercise-solutions/async-chat/book/src/images/async_2.svg +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - S - - - - - - - - - - - - - - - - ! - - - - - - - - - - - - - T - - - - - - T - - - - diff --git a/exercise-solutions/async-chat/book/src/images/horizontal_color.svg b/exercise-solutions/async-chat/book/src/images/horizontal_color.svg deleted file mode 100644 index 88bed32c..00000000 --- a/exercise-solutions/async-chat/book/src/images/horizontal_color.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - diff --git a/exercise-solutions/async-chat/book/src/images/icon_color.svg b/exercise-solutions/async-chat/book/src/images/icon_color.svg deleted file mode 100644 index 90dccdee..00000000 --- a/exercise-solutions/async-chat/book/src/images/icon_color.svg +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - diff --git a/exercise-solutions/async-chat/book/src/images/vertical_color.svg b/exercise-solutions/async-chat/book/src/images/vertical_color.svg deleted file mode 100644 index 14bd065c..00000000 --- a/exercise-solutions/async-chat/book/src/images/vertical_color.svg +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - diff --git a/exercise-solutions/async-chat/book/src/introduction.md b/exercise-solutions/async-chat/book/src/introduction.md deleted file mode 100644 index 19498db2..00000000 --- a/exercise-solutions/async-chat/book/src/introduction.md +++ /dev/null @@ -1,9 +0,0 @@ -# Introduction - -![async-std logo](./images/horizontal_color.svg) - -This book serves as high-level documentation for `async-std` and a way of learning async programming in Rust through it. As such, it focuses on the `async-std` API and the task model it gives you. - -Please note that the Rust project provides its own book on asynchronous programming, called ["Asynchronous Programming in Rust"][async-book], which we highly recommend reading along with this book, as it provides a different, wider view on the topic. - -[async-book]: https://rust-lang.github.io/async-book/ diff --git a/exercise-solutions/async-chat/book/src/overview/async-std.md b/exercise-solutions/async-chat/book/src/overview/async-std.md deleted file mode 100644 index 0086599f..00000000 --- a/exercise-solutions/async-chat/book/src/overview/async-std.md +++ /dev/null @@ -1,7 +0,0 @@ -# Welcome to `async-std` - -`async-std`, along with its [supporting libraries][organization], is a library making your life in async programming easier. It provides fundamental implementations for downstream libraries and applications alike. The name reflects the approach of this library: it is as closely modeled to the Rust main standard library as possible, replacing all components by async counterparts. - -`async-std` provides an interface to all important primitives: filesystem operations, network operations and concurrency basics like timers. It also exposes a `task` in a model similar to the `thread` module found in the Rust standard lib. But it does not only include I/O primitives, but also `async/await` compatible versions of primitives like `Mutex`. - -[organization]: https://github.com/async-rs diff --git a/exercise-solutions/async-chat/book/src/overview/stability-guarantees.md b/exercise-solutions/async-chat/book/src/overview/stability-guarantees.md deleted file mode 100644 index 8c14e20f..00000000 --- a/exercise-solutions/async-chat/book/src/overview/stability-guarantees.md +++ /dev/null @@ -1,40 +0,0 @@ -# Stability and SemVer - -`async-std` follows https://semver.org/. - -In short: we are versioning our software as `MAJOR.MINOR.PATCH`. We increase the: - -* MAJOR version when there are incompatible API changes, -* MINOR version when we introduce functionality in a backwards-compatible manner -* PATCH version when we make backwards-compatible bug fixes - -We will provide migration documentation between major versions. - -## Future expectations - -`async-std` uses its own implementations of the following concepts: - -* `Read` -* `Write` -* `Seek` -* `BufRead` -* `Stream` - -For integration with the ecosystem, all types implementing these traits also have an implementation of the corresponding interfaces in the `futures-rs` library. -Please note that our SemVer guarantees don't extend to usage of those interfaces. We expect those to be conservatively updated and in lockstep. - -## Minimum version policy - -The current tentative policy is that the minimum Rust version required to use this crate can be increased in minor version updates. For example, if `async-std` 1.0 requires Rust 1.37.0, then `async-std` 1.0.z for all values of z will also require Rust 1.37.0 or newer. However, `async-std` 1.y for y > 0 may require a newer minimum version of Rust. - -In general, this crate will be conservative with respect to the minimum supported version of Rust. With `async/await` being a new feature though, we will track changes in a measured pace initially. - -## Security fixes - -Security fixes will be applied to _all_ minor branches of this library in all _supported_ major revisions. This policy might change in the future, in which case we give a notice at least _3 months_ ahead. - -## Credits - -This policy is based on [BurntSushi's regex crate][regex-policy]. - -[regex-policy]: https://github.com/rust-lang/regex#minimum-rust-version-policy diff --git a/exercise-solutions/async-chat/book/src/patterns.md b/exercise-solutions/async-chat/book/src/patterns.md deleted file mode 100644 index a19b81b4..00000000 --- a/exercise-solutions/async-chat/book/src/patterns.md +++ /dev/null @@ -1,5 +0,0 @@ -# Patterns - -This section documents small, useful patterns. - -It is intended to be read at a glance, allowing you to get back when you have a problem. \ No newline at end of file diff --git a/exercise-solutions/async-chat/book/src/patterns/accept-loop.md b/exercise-solutions/async-chat/book/src/patterns/accept-loop.md deleted file mode 100644 index 4bc43e7d..00000000 --- a/exercise-solutions/async-chat/book/src/patterns/accept-loop.md +++ /dev/null @@ -1,266 +0,0 @@ -# Production-Ready Accept Loop - -A production-ready accept loop needs the following things: -1. Handling errors -2. Limiting the number of simultanteous connections to avoid deny-of-service - (DoS) attacks - - -## Handling errors - -There are two kinds of errors in an accept loop: -1. Per-connection errors. The system uses them to notify that there was a - connection in the queue and it's dropped by the peer. Subsequent connections - can be already queued so next connection must be accepted immediately. -2. Resource shortages. When these are encountered it doesn't make sense to - accept the next socket immediately. But the listener stays active, so you server - should try to accept socket later. - -Here is the example of a per-connection error (printed in normal and debug mode): -``` -Error: Connection reset by peer (os error 104) -Error: Os { code: 104, kind: ConnectionReset, message: "Connection reset by peer" } -``` - -And the following is the most common example of a resource shortage error: -``` -Error: Too many open files (os error 24) -Error: Os { code: 24, kind: Other, message: "Too many open files" } -``` - -### Testing Application - -To test your application for these errors try the following (this works -on unixes only). - -Lower limits and start the application: -``` -$ ulimit -n 100 -$ cargo run --example your_app - Compiling your_app v0.1.0 (/work) - Finished dev [unoptimized + debuginfo] target(s) in 5.47s - Running `target/debug/examples/your_app` -Server is listening on: http://127.0.0.1:1234 -``` -Then in another console run the [`wrk`] benchmark tool: -``` -$ wrk -c 1000 http://127.0.0.1:1234 -Running 10s test @ http://localhost:8080/ - 2 threads and 1000 connections -$ telnet localhost 1234 -Trying ::1... -Connected to localhost. -``` - -Important is to check the following things: - -1. The application doesn't crash on error (but may log errors, see below) -2. It's possible to connect to the application again once load is stopped - (few seconds after `wrk`). This is what `telnet` does in example above, - make sure it prints `Connected to `. -3. The `Too many open files` error is logged in the appropriate log. This - requires to set "maximum number of simultaneous connections" parameter (see - below) of your application to a value greater then `100` for this example. -4. Check CPU usage of the app while doing a test. It should not occupy 100% - of a single CPU core (it's unlikely that you can exhaust CPU by 1000 - connections in Rust, so this means error handling is not right). - -#### Testing non-HTTP applications - -If it's possible, use the appropriate benchmark tool and set the appropriate -number of connections. For example `redis-benchmark` has a `-c` parameter for -that, if you implement redis protocol. - -Alternatively, can still use `wrk`, just make sure that connection is not -immediately closed. If it is, put a temporary timeout before handing -the connection to the protocol handler, like this: - -```rust,edition2018 -# extern crate async_std; -# use std::time::Duration; -# use async_std::{ -# net::{TcpListener, ToSocketAddrs}, -# prelude::*, -# }; -# -# type Result = std::result::Result>; -# -#async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { -# let listener = TcpListener::bind(addr).await?; -# let mut incoming = listener.incoming(); -while let Some(stream) = incoming.next().await { - task::spawn(async { - task::sleep(Duration::from_secs(10)).await; // 1 - connection_loop(stream).await; - }); -} -# Ok(()) -# } -``` - -1. Make sure the sleep coroutine is inside the spawned task, not in the loop. - -[`wrk`]: https://github.com/wg/wrk - - -### Handling Errors Manually - -Here is how basic accept loop could look like: - -```rust,edition2018 -# extern crate async_std; -# use std::time::Duration; -# use async_std::{ -# net::{TcpListener, ToSocketAddrs}, -# prelude::*, -# }; -# -# type Result = std::result::Result>; -# -async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { - let listener = TcpListener::bind(addr).await?; - let mut incoming = listener.incoming(); - while let Some(result) = incoming.next().await { - let stream = match result { - Err(ref e) if is_connection_error(e) => continue, // 1 - Err(e) => { - eprintln!("Error: {}. Pausing for 500ms.", e); // 3 - task::sleep(Duration::from_millis(500)).await; // 2 - continue; - } - Ok(s) => s, - }; - // body - } - Ok(()) -} -``` - -1. Ignore per-connection errors. -2. Sleep and continue on resource shortage. -3. It's important to log the message, because these errors commonly mean the - misconfiguration of the system and are helpful for operations people running - the application. - -Be sure to [test your application](#testing-application). - - -### External Crates - -The crate [`async-listen`] has a helper to achieve this task: -```rust,edition2018 -# extern crate async_std; -# extern crate async_listen; -# use std::time::Duration; -# use async_std::{ -# net::{TcpListener, ToSocketAddrs}, -# prelude::*, -# }; -# -# type Result = std::result::Result>; -# -use async_listen::{ListenExt, error_hint}; - -async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { - - let listener = TcpListener::bind(addr).await?; - let mut incoming = listener - .incoming() - .log_warnings(log_accept_error) // 1 - .handle_errors(Duration::from_millis(500)); - while let Some(socket) = incoming.next().await { // 2 - // body - } - Ok(()) -} - -fn log_accept_error(e: &io::Error) { - eprintln!("Error: {}. Listener paused for 0.5s. {}", e, error_hint(e)) // 3 -} -``` - -1. Logs resource shortages (`async-listen` calls them warnings). If you use - `log` crate or any other in your app this should go to the log. -2. Stream yields sockets without `Result` wrapper after `handle_errors` because - all errors are already handled. -3. Together with the error we print a hint, which explains some errors for end - users. For example, it recommends increasing open file limit and gives - a link. - -[`async-listen`]: https://crates.io/crates/async-listen/ - -Be sure to [test your application](#testing-application). - - -## Connections Limit - -Even if you've applied everything described in -[Handling Errors](#handling-errors) section, there is still a problem. - -Let's imagine you have a server that needs to open a file to process -client request. At some point, you might encounter the following situation: - -1. There are as many client connection as max file descriptors allowed for - the application. -2. Listener gets `Too many open files` error so it sleeps. -3. Some client sends a request via the previously open connection. -4. Opening a file to serve request fails, because of the same - `Too many open files` error, until some other client drops a connection. - -There are many more possible situations, this is just a small illustation that -limiting number of connections is very useful. Generally, it's one of the ways -to control resources used by a server and avoiding some kinds of deny of -service (DoS) attacks. - -### `async-listen` crate - -Limiting maximum number of simultaneous connections with [`async-listen`] -looks like the following: - -```rust,edition2018 -# extern crate async_std; -# extern crate async_listen; -# use std::time::Duration; -# use async_std::{ -# net::{TcpListener, TcpStream, ToSocketAddrs}, -# prelude::*, -# }; -# -# type Result = std::result::Result>; -# -use async_listen::{ListenExt, Token, error_hint}; - -async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { - - let listener = TcpListener::bind(addr).await?; - let mut incoming = listener - .incoming() - .log_warnings(log_accept_error) - .handle_errors(Duration::from_millis(500)) // 1 - .backpressure(100); - while let Some((token, socket)) = incoming.next().await { // 2 - task::spawn(async move { - connection_loop(&token, stream).await; // 3 - }); - } - Ok(()) -} -async fn connection_loop(_token: &Token, stream: TcpStream) { // 4 - // ... -} -# fn log_accept_error(e: &io::Error) { -# eprintln!("Error: {}. Listener paused for 0.5s. {}", e, error_hint(e)); -# } -``` - -1. We need to handle errors first, because [`backpressure`] helper expects - stream of `TcpStream` rather than `Result`. -2. The token yielded by a new stream is what is counted by backpressure helper. - I.e. if you drop a token, new connection can be established. -3. We give the connection loop a reference to token to bind token's lifetime to - the lifetime of the connection. -4. The token itsellf in the function can be ignored, hence `_token` - -[`backpressure`]: https://docs.rs/async-listen/0.1.2/async_listen/trait.ListenExt.html#method.backpressure - -Be sure to [test this behavior](#testing-application). diff --git a/exercise-solutions/async-chat/book/src/patterns/accepting-concurrent-requests.md b/exercise-solutions/async-chat/book/src/patterns/accepting-concurrent-requests.md deleted file mode 100644 index b984183c..00000000 --- a/exercise-solutions/async-chat/book/src/patterns/accepting-concurrent-requests.md +++ /dev/null @@ -1 +0,0 @@ -# Accepting requests diff --git a/exercise-solutions/async-chat/book/src/patterns/async-read-write.md b/exercise-solutions/async-chat/book/src/patterns/async-read-write.md deleted file mode 100644 index 79354298..00000000 --- a/exercise-solutions/async-chat/book/src/patterns/async-read-write.md +++ /dev/null @@ -1 +0,0 @@ -# Async read/write diff --git a/exercise-solutions/async-chat/book/src/patterns/background-tasks.md b/exercise-solutions/async-chat/book/src/patterns/background-tasks.md deleted file mode 100644 index 02571b26..00000000 --- a/exercise-solutions/async-chat/book/src/patterns/background-tasks.md +++ /dev/null @@ -1 +0,0 @@ -# Background Tasks diff --git a/exercise-solutions/async-chat/book/src/patterns/fork-join.md b/exercise-solutions/async-chat/book/src/patterns/fork-join.md deleted file mode 100644 index 4709bb20..00000000 --- a/exercise-solutions/async-chat/book/src/patterns/fork-join.md +++ /dev/null @@ -1 +0,0 @@ -# Fork/Join diff --git a/exercise-solutions/async-chat/book/src/patterns/proper-shutdown.md b/exercise-solutions/async-chat/book/src/patterns/proper-shutdown.md deleted file mode 100644 index 49ba1f43..00000000 --- a/exercise-solutions/async-chat/book/src/patterns/proper-shutdown.md +++ /dev/null @@ -1 +0,0 @@ -# Proper Shutdown diff --git a/exercise-solutions/async-chat/book/src/patterns/small-patterns.md b/exercise-solutions/async-chat/book/src/patterns/small-patterns.md deleted file mode 100644 index 1bc1d907..00000000 --- a/exercise-solutions/async-chat/book/src/patterns/small-patterns.md +++ /dev/null @@ -1,16 +0,0 @@ -# Small Patterns - -A collection of small, useful patterns. - -## Splitting streams - -`async-std` doesn't provide a `split()` method on `io` handles. Instead, splitting a stream into a read and write half can be done like this: - -```rust,edition2018 -# extern crate async_std; -use async_std::{io, net::TcpStream}; -async fn echo(stream: TcpStream) { - let (reader, writer) = &mut (&stream, &stream); - io::copy(reader, writer).await; -} -``` diff --git a/exercise-solutions/async-chat/book/src/patterns/testing.md b/exercise-solutions/async-chat/book/src/patterns/testing.md deleted file mode 100644 index f00b526a..00000000 --- a/exercise-solutions/async-chat/book/src/patterns/testing.md +++ /dev/null @@ -1 +0,0 @@ -# Testing diff --git a/exercise-solutions/async-chat/book/src/security/index.md b/exercise-solutions/async-chat/book/src/security/index.md deleted file mode 100644 index 02fef4c0..00000000 --- a/exercise-solutions/async-chat/book/src/security/index.md +++ /dev/null @@ -1,12 +0,0 @@ -# Security - -Writing a highly performant async core library is a task involving some instances of unsafe code. - -We take great care in vetting all unsafe code included in `async-std` and do follow generally accepted practices. - -In the case that you find a security-related bug in our library, please get in touch with our [security contact][security-policy]. - -Patches improving the resilience of the library or the testing setup are happily accepted on our [github org][github]. - -[security-policy]: /security/policy -[github]: https://github.com/async-rs diff --git a/exercise-solutions/async-chat/book/src/security/policy.md b/exercise-solutions/async-chat/book/src/security/policy.md deleted file mode 100644 index 06a08b48..00000000 --- a/exercise-solutions/async-chat/book/src/security/policy.md +++ /dev/null @@ -1,66 +0,0 @@ -# Policy - -Safety is one of the core principles of what we do, and to that end, we would like to ensure that async-std has a secure implementation. Thank you for taking the time to responsibly disclose any issues you find. - -All security bugs in async-std distribution should be reported by email to florian.gilcher@ferrous-systems.com. This list is delivered to a small security team. Your email will be acknowledged within 24 hours, and you’ll receive a more detailed response to your email within 48 hours indicating the next steps in handling your report. If you would like, you can encrypt your report using our public key. This key is also On MIT’s keyserver and reproduced below. - -Be sure to use a descriptive subject line to avoid having your report be missed. After the initial reply to your report, the security team will endeavor to keep you informed of the progress being made towards a fix and full announcement. As recommended by [RFPolicy][rf-policy], these updates will be sent at least every five days. In reality, this is more likely to be every 24-48 hours. - -If you have not received a reply to your email within 48 hours, or have not heard from the security team for the past five days, there are a few steps you can take (in order): - -* Post on our Community forums - -Please note that the discussion forums are public areas. When escalating in these venues, please do not discuss your issue. Simply say that you’re trying to get a hold of someone from the security team. - -[rf-policy]: https://en.wikipedia.org/wiki/RFPolicy - -## Disclosure policy - -The async-std project has a 5 step disclosure process. - -* The security report is received and is assigned a primary handler. This person will coordinate the fix and release process. -* The problem is confirmed and a list of all affected versions is determined. -* Code is audited to find any potential similar problems. -* Fixes are prepared for all releases which are still under maintenance. These fixes are not committed to the public repository but rather held locally pending the announcement. -* On the embargo date, the changes are pushed to the public repository and new builds are deployed to crates.io. Within 6 hours, a copy of the advisory will be published on the the async.rs blog. - -This process can take some time, especially when coordination is required with maintainers of other projects. Every effort will be made to handle the bug in as timely a manner as possible, however it's important that we follow the release process above to ensure that the disclosure is handled in a consistent manner. - -## Credits - -This policy is adapted from the [Rust project](https://www.rust-lang.org/policies/security) security policy. - -## PGP Key - -```text ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQENBF1Wu/ABCADJaGt4HwSlqKB9BGHWYKZj/6mTMbmc29vsEOcCSQKo6myCf9zc -sasWAttep4FAUDX+MJhVbBTSq9M1YVxp33Qh5AF0t9SnJZnbI+BZuGawcHDL01xE -bE+8bcA2+szeTTUZCeWwsaoTd/2qmQKvpUCBQp7uBs/ITO/I2q7+xCGXaOHZwUKc -H8SUBLd35nYFtjXAeejoZVkqG2qEjrc9bkZAwxFXi7Fw94QdkNLaCjNfKxZON/qP -A3WOpyWPr3ERk5C5prjEAvrW8kdqpTRjdmzQjsr8UEXb5GGEOo93N4OLZVQ2mXt9 -dfn++GOnOk7sTxvfiDH8Ru5o4zCtKgO+r5/LABEBAAG0UkZsb3JpYW4gR2lsY2hl -ciAoU2VjdXJpdHkgY29udGFjdCBhc3luYy1zdGQpIDxmbG9yaWFuLmdpbGNoZXJA -ZmVycm91cy1zeXN0ZW1zLmNvbT6JATgEEwECACIFAl1Wu/ACGwMGCwkIBwMCBhUI -AgkKCwQWAgMBAh4BAheAAAoJEACXY97PwLtSc0AH/18yvrElVOkG0ADWX7l+JKHH -nMQtYj0Auop8d6TuKBbpwtYhwELrQoITDMV7f2XEnchNsvYxAyBZhIISmXeJboE1 -KzZD1O+4QPXRcXhj+QNsKQ680mrgZXgAI2Y4ptIW9Vyw3jiHu/ZVopvDAt4li+up -3fRJGPAvGu+tclpJmA+Xam23cDj89M7/wHHgKIyT59WgFwyCgibL+NHKwg2Unzou -9uyZQnq6hf62sQTWEZIAr9BQpKmluplNIJHDeECWzZoE9ucE2ZXsq5pq9qojsAMK -yRdaFdpBcD/AxtrTKFeXGS7X7LqaljY/IFBEdJOqVNWpqSLjGWqjSLIEsc1AB0K5 -AQ0EXVa78AEIAJMxBOEEW+2c3CcjFuUfcRsoBsFH3Vk+GwCbjIpNHq/eAvS1yy2L -u10U5CcT5Xb6be3AeCYv00ZHVbEi6VwoauVCSX8qDjhVzQxvNLgQ1SduobjyF6t8 -3M/wTija6NvMKszyw1l2oHepxSMLej1m49DyCDFNiZm5rjQcYnFT4J71syxViqHF -v2fWCheTrHP3wfBAt5zyDet7IZd/EhYAK6xXEwr9nBPjfbaVexm2B8K6hOPNj0Bp -OKm4rcOj7JYlcxrwhMvNnwEue7MqH1oXAsoaC1BW+qs4acp/hHpesweL6Rcg1pED -OJUQd3UvRsqRK0EsorDu0oj5wt6Qp3ZEbPMAEQEAAYkBHwQYAQIACQUCXVa78AIb -DAAKCRAAl2Pez8C7Uv8bB/9scRm2wvzHLbFtcEHaHvlKO1yYfSVqKqJzIKHc7pM2 -+szM8JVRTxAbzK5Xih9SB5xlekixxO2UCJI5DkJ/ir/RCcg+/CAQ8iLm2UcYAgJD -TocKiR5gjNAvUDI4tMrDLLdF+7+RCQGc7HBSxFiNBJVGAztGVh1+cQ0zaCX6Tt33 -1EQtyRcPID0m6+ip5tCJN0dILC0YcwzXGrSgjB03JqItIyJEucdQz6UB84TIAGku -JJl4tktgD9T7Rb5uzRhHCSbLy89DQVvCcKD4B94ffuDW3HO8n8utDusOiZuG4BUf -WdFy6/gTLNiFbTzkq1BBJQMN1nBwGs1sn63RRgjumZ1N -=dIcF ------END PGP PUBLIC KEY BLOCK----- -``` diff --git a/exercise-solutions/async-chat/docs/SUMMARY.md b/exercise-solutions/async-chat/docs/SUMMARY.md new file mode 100644 index 00000000..190c69de --- /dev/null +++ b/exercise-solutions/async-chat/docs/SUMMARY.md @@ -0,0 +1,12 @@ +# Summary + +- [Tutorial: Implementing a chat](./index.md) + - [Specification and Getting started](./specification.md) + - [Writing an Accept Loop](./accept_loop.md) + - [Receiving Messages](./receiving_messages.md) + - [Sending Messages](./sending_messages.md) + - [Connecting Readers and Writers](./connecting_readers_and_writers.md) + - [All Together](./all_together.md) + - [Clean Shutdown](./clean_shutdown.md) + - [Handling Disconnection](./handling_disconnection.md) + - [Implementing a Client](./implementing_a_client.md) diff --git a/exercise-solutions/async-chat/book/src/tutorial/accept_loop.md b/exercise-solutions/async-chat/docs/accept_loop.md similarity index 100% rename from exercise-solutions/async-chat/book/src/tutorial/accept_loop.md rename to exercise-solutions/async-chat/docs/accept_loop.md diff --git a/exercise-solutions/async-chat/book/src/tutorial/all_together.md b/exercise-solutions/async-chat/docs/all_together.md similarity index 100% rename from exercise-solutions/async-chat/book/src/tutorial/all_together.md rename to exercise-solutions/async-chat/docs/all_together.md diff --git a/exercise-solutions/async-chat/book/src/tutorial/clean_shutdown.md b/exercise-solutions/async-chat/docs/clean_shutdown.md similarity index 100% rename from exercise-solutions/async-chat/book/src/tutorial/clean_shutdown.md rename to exercise-solutions/async-chat/docs/clean_shutdown.md diff --git a/exercise-solutions/async-chat/book/src/tutorial/connecting_readers_and_writers.md b/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md similarity index 100% rename from exercise-solutions/async-chat/book/src/tutorial/connecting_readers_and_writers.md rename to exercise-solutions/async-chat/docs/connecting_readers_and_writers.md diff --git a/exercise-solutions/async-chat/book/src/tutorial/handling_disconnection.md b/exercise-solutions/async-chat/docs/handling_disconnection.md similarity index 100% rename from exercise-solutions/async-chat/book/src/tutorial/handling_disconnection.md rename to exercise-solutions/async-chat/docs/handling_disconnection.md diff --git a/exercise-solutions/async-chat/book/src/tutorial/implementing_a_client.md b/exercise-solutions/async-chat/docs/implementing_a_client.md similarity index 100% rename from exercise-solutions/async-chat/book/src/tutorial/implementing_a_client.md rename to exercise-solutions/async-chat/docs/implementing_a_client.md diff --git a/exercise-solutions/async-chat/book/src/tutorial/index.md b/exercise-solutions/async-chat/docs/index.md similarity index 100% rename from exercise-solutions/async-chat/book/src/tutorial/index.md rename to exercise-solutions/async-chat/docs/index.md diff --git a/exercise-solutions/async-chat/book/src/tutorial/receiving_messages.md b/exercise-solutions/async-chat/docs/receiving_messages.md similarity index 100% rename from exercise-solutions/async-chat/book/src/tutorial/receiving_messages.md rename to exercise-solutions/async-chat/docs/receiving_messages.md diff --git a/exercise-solutions/async-chat/book/src/tutorial/sending_messages.md b/exercise-solutions/async-chat/docs/sending_messages.md similarity index 100% rename from exercise-solutions/async-chat/book/src/tutorial/sending_messages.md rename to exercise-solutions/async-chat/docs/sending_messages.md diff --git a/exercise-solutions/async-chat/book/src/tutorial/specification.md b/exercise-solutions/async-chat/docs/specification.md similarity index 100% rename from exercise-solutions/async-chat/book/src/tutorial/specification.md rename to exercise-solutions/async-chat/docs/specification.md From efe4dfa16f471506086a91242ce566f3b87ae886 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 10:25:49 +0200 Subject: [PATCH 11/36] specification --- exercise-solutions/async-chat/docs/specification.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exercise-solutions/async-chat/docs/specification.md b/exercise-solutions/async-chat/docs/specification.md index 7b1a0167..a725bee0 100644 --- a/exercise-solutions/async-chat/docs/specification.md +++ b/exercise-solutions/async-chat/docs/specification.md @@ -42,6 +42,5 @@ Add the following lines to `Cargo.toml`: ```toml [dependencies] -futures = "0.3.0" -async-std = "1" +tokio = { version = "1", features = ["full"] } ``` From b9b9a2de78d394c0bf2af4fd6b54ba0e162d4bf3 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 10:45:47 +0200 Subject: [PATCH 12/36] index --- exercise-solutions/async-chat/docs/index.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/exercise-solutions/async-chat/docs/index.md b/exercise-solutions/async-chat/docs/index.md index 076ecf41..f0ccf155 100644 --- a/exercise-solutions/async-chat/docs/index.md +++ b/exercise-solutions/async-chat/docs/index.md @@ -9,6 +9,4 @@ How will it handle them disconnecting? How will it distribute the messages? -This tutorial explains how to write a chat server in `async-std`. - -You can also find the tutorial in [our repository](https://github.com/async-rs/async-std/blob/HEAD/examples/a-chat). +This tutorial explains how to write a chat server in `tokio`. From dee602bd7feab867649864cc4fb8a643f1d0ff1a Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 10:47:37 +0200 Subject: [PATCH 13/36] Use 2021 edition --- exercise-solutions/async-chat/docs/accept_loop.md | 8 ++++---- exercise-solutions/async-chat/docs/all_together.md | 2 +- exercise-solutions/async-chat/docs/clean_shutdown.md | 4 ++-- .../async-chat/docs/connecting_readers_and_writers.md | 2 +- .../async-chat/docs/handling_disconnection.md | 6 +++--- .../async-chat/docs/implementing_a_client.md | 2 +- exercise-solutions/async-chat/docs/receiving_messages.md | 6 +++--- exercise-solutions/async-chat/docs/sending_messages.md | 2 +- exercise-solutions/async-chat/src/server.rs | 6 ++---- 9 files changed, 18 insertions(+), 20 deletions(-) diff --git a/exercise-solutions/async-chat/docs/accept_loop.md b/exercise-solutions/async-chat/docs/accept_loop.md index dc748bb4..0011e4a8 100644 --- a/exercise-solutions/async-chat/docs/accept_loop.md +++ b/exercise-solutions/async-chat/docs/accept_loop.md @@ -4,7 +4,7 @@ Let's implement the scaffold of the server: a loop that binds a TCP socket to an First of all, let's add required import boilerplate: -```rust,edition2018 +```rust # extern crate async_std; use async_std::{ prelude::*, // 1 @@ -25,7 +25,7 @@ type Result = std::result::Result Now we can write the server's accept loop: -```rust,edition2018 +```rust # extern crate async_std; # use async_std::{ # net::{TcpListener, ToSocketAddrs}, @@ -52,7 +52,7 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { // 1 Mirroring API of `std` is an explicit design goal of `async_std`. 3. Here, we would like to iterate incoming sockets, just how one would do in `std`: -```rust,edition2018,should_panic +```rust,should_panic let listener: std::net::TcpListener = unimplemented!(); for stream in listener.incoming() { } @@ -63,7 +63,7 @@ For this reason we have to implement the loop manually, by using `while let Some Finally, let's add main: -```rust,edition2018 +```rust # extern crate async_std; # use async_std::{ # net::{TcpListener, ToSocketAddrs}, diff --git a/exercise-solutions/async-chat/docs/all_together.md b/exercise-solutions/async-chat/docs/all_together.md index b8174d30..571cfe7c 100644 --- a/exercise-solutions/async-chat/docs/all_together.md +++ b/exercise-solutions/async-chat/docs/all_together.md @@ -2,7 +2,7 @@ At this point, we only need to start the broker to get a fully-functioning (in the happy case!) chat: -```rust,edition2018 +```rust # extern crate async_std; # extern crate futures; use async_std::{ diff --git a/exercise-solutions/async-chat/docs/clean_shutdown.md b/exercise-solutions/async-chat/docs/clean_shutdown.md index 0937d708..4dee8736 100644 --- a/exercise-solutions/async-chat/docs/clean_shutdown.md +++ b/exercise-solutions/async-chat/docs/clean_shutdown.md @@ -20,7 +20,7 @@ In `a-chat`, we already have an unidirectional flow of messages: `reader -> brok However, we never wait for broker and writers, which might cause some messages to get dropped. Let's add waiting to the server: -```rust,edition2018 +```rust # extern crate async_std; # extern crate futures; # use async_std::{ @@ -153,7 +153,7 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { And to the broker: -```rust,edition2018 +```rust # extern crate async_std; # extern crate futures; # use async_std::{ diff --git a/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md b/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md index 921cf90c..6e743817 100644 --- a/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md +++ b/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md @@ -10,7 +10,7 @@ We can create a dedicated broker task which owns the `peers` map and communicate By hiding `peers` inside such an "actor" task, we remove the need for mutexes and also make the serialization point explicit. The order of events "Bob sends message to Alice" and "Alice joins" is determined by the order of the corresponding events in the broker's event queue. -```rust,edition2018 +```rust # extern crate async_std; # extern crate futures; # use async_std::{ diff --git a/exercise-solutions/async-chat/docs/handling_disconnection.md b/exercise-solutions/async-chat/docs/handling_disconnection.md index b6e53641..cd38d9ee 100644 --- a/exercise-solutions/async-chat/docs/handling_disconnection.md +++ b/exercise-solutions/async-chat/docs/handling_disconnection.md @@ -17,7 +17,7 @@ This way, we statically guarantee that we issue shutdown exactly once, even if w First, let's add a shutdown channel to the `connection_loop`: -```rust,edition2018 +```rust # extern crate async_std; # extern crate futures; # use async_std::net::TcpStream; @@ -67,7 +67,7 @@ async fn connection_loop(mut broker: Sender, stream: Arc) -> R In the `connection_writer_loop`, we now need to choose between shutdown and message channels. We use the `select` macro for this purpose: -```rust,edition2018 +```rust # extern crate async_std; # extern crate futures; # use async_std::{net::TcpStream, prelude::*}; @@ -117,7 +117,7 @@ This also allows us to establish a useful invariant that the message channel str The final code looks like this: -```rust,edition2018 +```rust # extern crate async_std; # extern crate futures; use async_std::{ diff --git a/exercise-solutions/async-chat/docs/implementing_a_client.md b/exercise-solutions/async-chat/docs/implementing_a_client.md index ba9d6f33..6f145d3e 100644 --- a/exercise-solutions/async-chat/docs/implementing_a_client.md +++ b/exercise-solutions/async-chat/docs/implementing_a_client.md @@ -12,7 +12,7 @@ Programming this with threads is cumbersome, especially when implementing a clea With async, the `select!` macro is all that is needed. -```rust,edition2018 +```rust # extern crate async_std; # extern crate futures; use async_std::{ diff --git a/exercise-solutions/async-chat/docs/receiving_messages.md b/exercise-solutions/async-chat/docs/receiving_messages.md index 036bc459..0a5b6260 100644 --- a/exercise-solutions/async-chat/docs/receiving_messages.md +++ b/exercise-solutions/async-chat/docs/receiving_messages.md @@ -7,7 +7,7 @@ We need to: 2. interpret the first line as a login 3. parse the rest of the lines as a `login: message` -```rust,edition2018 +```rust # extern crate async_std; # use async_std::{ # net::{TcpListener, ToSocketAddrs}, @@ -75,7 +75,7 @@ One serious problem in the above solution is that, while we correctly propagate That is, `task::spawn` does not return an error immediately (it can't, it needs to run the future to completion first), only after it is joined. We can "fix" it by waiting for the task to be joined, like this: -```rust,edition2018 +```rust # #![feature(async_closure)] # extern crate async_std; # use async_std::{ @@ -125,7 +125,7 @@ That is, a flaky internet connection of one peer brings down the whole chat room A correct way to handle client errors in this case is log them, and continue serving other clients. So let's use a helper function for this: -```rust,edition2018 +```rust # extern crate async_std; # use async_std::{ # io, diff --git a/exercise-solutions/async-chat/docs/sending_messages.md b/exercise-solutions/async-chat/docs/sending_messages.md index 3f426d02..95254b5a 100644 --- a/exercise-solutions/async-chat/docs/sending_messages.md +++ b/exercise-solutions/async-chat/docs/sending_messages.md @@ -11,7 +11,7 @@ So let's create a `connection_writer_loop` task which receives messages over a c This task would be the point of serialization of messages. if Alice and Charley send two messages to Bob at the same time, Bob will see the messages in the same order as they arrive in the channel. -```rust,edition2018 +```rust # extern crate async_std; # extern crate futures; # use async_std::{ diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index 5c4ee448..3f708883 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -3,12 +3,10 @@ use std::{ future::Future, }; -use tokio::sync::{mpsc, oneshot}; - use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - net::tcp::OwnedWriteHalf, - net::{TcpListener, TcpStream, ToSocketAddrs}, + net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, + sync::{mpsc, oneshot}, task, }; From 401d546bb0bca5c379f62b2ca687b1a3d4e77aa3 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 11:15:38 +0200 Subject: [PATCH 14/36] accept_loop --- .../async-chat/docs/accept_loop.md | 61 ++++++------------- 1 file changed, 20 insertions(+), 41 deletions(-) diff --git a/exercise-solutions/async-chat/docs/accept_loop.md b/exercise-solutions/async-chat/docs/accept_loop.md index 0011e4a8..9a02c400 100644 --- a/exercise-solutions/async-chat/docs/accept_loop.md +++ b/exercise-solutions/async-chat/docs/accept_loop.md @@ -5,20 +5,22 @@ Let's implement the scaffold of the server: a loop that binds a TCP socket to an First of all, let's add required import boilerplate: ```rust -# extern crate async_std; -use async_std::{ - prelude::*, // 1 +# extern crate tokio; +use std::future::Future, // 1 +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, // 1 + net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, // 3 + sync::{mpsc, oneshot}, task, // 2 - net::{TcpListener, ToSocketAddrs}, // 3 }; type Result = std::result::Result>; // 4 ``` -1. `prelude` re-exports some traits required to work with futures and streams. +1. Import some traits required to work with futures and streams. 2. The `task` module roughly corresponds to the `std::thread` module, but tasks are much lighter weight. A single thread can run many tasks. -3. For the socket type, we use `TcpListener` from `async_std`, which is just like `std::net::TcpListener`, but is non-blocking and uses `async` API. +3. For the socket type, we use `TcpListener` from `tokio`, which is just like `std::net::TcpListener`, but is non-blocking and uses `async` API. 4. We will skip implementing comprehensive error handling in this example. To propagate the errors, we will use a boxed error trait object. Do you know that there's `From<&'_ str> for Box` implementation in stdlib, which allows you to use strings with `?` operator? @@ -26,19 +28,14 @@ type Result = std::result::Result Now we can write the server's accept loop: ```rust -# extern crate async_std; -# use async_std::{ -# net::{TcpListener, ToSocketAddrs}, -# prelude::*, -# }; -# +# extern crate tokio; +# use tokio::net::{TcpListener, ToSocketAddrs}; # type Result = std::result::Result>; # async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { // 1 - let listener = TcpListener::bind(addr).await?; // 2 - let mut incoming = listener.incoming(); - while let Some(stream) = incoming.next().await { // 3 + + while let Ok((stream, _socket_addr)) = listener.accept().await { // 3 // TODO } Ok(()) @@ -49,43 +46,26 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { // 1 2. `TcpListener::bind` call returns a future, which we `.await` to extract the `Result`, and then `?` to get a `TcpListener`. Note how `.await` and `?` work nicely together. This is exactly how `std::net::TcpListener` works, but with `.await` added. - Mirroring API of `std` is an explicit design goal of `async_std`. -3. Here, we would like to iterate incoming sockets, just how one would do in `std`: +3. Here, we would like to iterate incoming sockets, similar to how one would do in `std`: ```rust,should_panic let listener: std::net::TcpListener = unimplemented!(); for stream in listener.incoming() { + // ... } ``` -Unfortunately this doesn't quite work with `async` yet, because there's no support for `async` for-loops in the language yet. -For this reason we have to implement the loop manually, by using `while let Some(item) = iter.next().await` pattern. - Finally, let's add main: ```rust -# extern crate async_std; -# use async_std::{ -# net::{TcpListener, ToSocketAddrs}, -# prelude::*, -# task, -# }; -# +# extern crate tokio; +# use tokio::net::{ToSocketAddrs}; # type Result = std::result::Result>; +# async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> {} # -# async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { // 1 -# let listener = TcpListener::bind(addr).await?; // 2 -# let mut incoming = listener.incoming(); -# while let Some(stream) = incoming.next().await { // 3 -# // TODO -# } -# Ok(()) -# } -# -// main -fn run() -> Result<()> { - let fut = accept_loop("127.0.0.1:8080"); - task::block_on(fut) +#[tokio::main] +pub(crate) async fn main() -> Result<()> { + accept_loop("127.0.0.1:8080").await } ``` @@ -93,4 +73,3 @@ The crucial thing to realise that is in Rust, unlike other languages, calling an Async functions only construct futures, which are inert state machines. To start stepping through the future state-machine in an async function, you should use `.await`. In a non-async function, a way to execute a future is to hand it to the executor. -In this case, we use `task::block_on` to execute a future on the current thread and block until it's done. From eeb62b387c3de6663917a3a657f367ba46e81003 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 11:16:27 +0200 Subject: [PATCH 15/36] extern crate tokio --- exercise-solutions/async-chat/docs/all_together.md | 2 +- exercise-solutions/async-chat/docs/clean_shutdown.md | 4 ++-- .../async-chat/docs/connecting_readers_and_writers.md | 2 +- .../async-chat/docs/handling_disconnection.md | 6 +++--- exercise-solutions/async-chat/docs/implementing_a_client.md | 2 +- exercise-solutions/async-chat/docs/receiving_messages.md | 6 +++--- exercise-solutions/async-chat/docs/sending_messages.md | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/exercise-solutions/async-chat/docs/all_together.md b/exercise-solutions/async-chat/docs/all_together.md index 571cfe7c..92c97726 100644 --- a/exercise-solutions/async-chat/docs/all_together.md +++ b/exercise-solutions/async-chat/docs/all_together.md @@ -3,7 +3,7 @@ At this point, we only need to start the broker to get a fully-functioning (in the happy case!) chat: ```rust -# extern crate async_std; +# extern crate tokio; # extern crate futures; use async_std::{ io::BufReader, diff --git a/exercise-solutions/async-chat/docs/clean_shutdown.md b/exercise-solutions/async-chat/docs/clean_shutdown.md index 4dee8736..2384bc8d 100644 --- a/exercise-solutions/async-chat/docs/clean_shutdown.md +++ b/exercise-solutions/async-chat/docs/clean_shutdown.md @@ -21,7 +21,7 @@ However, we never wait for broker and writers, which might cause some messages t Let's add waiting to the server: ```rust -# extern crate async_std; +# extern crate tokio; # extern crate futures; # use async_std::{ # io::{self, BufReader}, @@ -154,7 +154,7 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { And to the broker: ```rust -# extern crate async_std; +# extern crate tokio; # extern crate futures; # use async_std::{ # io::{self, BufReader}, diff --git a/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md b/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md index 6e743817..760f290d 100644 --- a/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md +++ b/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md @@ -11,7 +11,7 @@ By hiding `peers` inside such an "actor" task, we remove the need for mutexes an The order of events "Bob sends message to Alice" and "Alice joins" is determined by the order of the corresponding events in the broker's event queue. ```rust -# extern crate async_std; +# extern crate tokio; # extern crate futures; # use async_std::{ # net::TcpStream, diff --git a/exercise-solutions/async-chat/docs/handling_disconnection.md b/exercise-solutions/async-chat/docs/handling_disconnection.md index cd38d9ee..45775751 100644 --- a/exercise-solutions/async-chat/docs/handling_disconnection.md +++ b/exercise-solutions/async-chat/docs/handling_disconnection.md @@ -18,7 +18,7 @@ This way, we statically guarantee that we issue shutdown exactly once, even if w First, let's add a shutdown channel to the `connection_loop`: ```rust -# extern crate async_std; +# extern crate tokio; # extern crate futures; # use async_std::net::TcpStream; # use futures::channel::mpsc; @@ -68,7 +68,7 @@ In the `connection_writer_loop`, we now need to choose between shutdown and mess We use the `select` macro for this purpose: ```rust -# extern crate async_std; +# extern crate tokio; # extern crate futures; # use async_std::{net::TcpStream, prelude::*}; # use futures::channel::mpsc; @@ -118,7 +118,7 @@ This also allows us to establish a useful invariant that the message channel str The final code looks like this: ```rust -# extern crate async_std; +# extern crate tokio; # extern crate futures; use async_std::{ io::BufReader, diff --git a/exercise-solutions/async-chat/docs/implementing_a_client.md b/exercise-solutions/async-chat/docs/implementing_a_client.md index 6f145d3e..4a12c7ac 100644 --- a/exercise-solutions/async-chat/docs/implementing_a_client.md +++ b/exercise-solutions/async-chat/docs/implementing_a_client.md @@ -13,7 +13,7 @@ With async, the `select!` macro is all that is needed. ```rust -# extern crate async_std; +# extern crate tokio; # extern crate futures; use async_std::{ io::{stdin, BufReader}, diff --git a/exercise-solutions/async-chat/docs/receiving_messages.md b/exercise-solutions/async-chat/docs/receiving_messages.md index 0a5b6260..615d525c 100644 --- a/exercise-solutions/async-chat/docs/receiving_messages.md +++ b/exercise-solutions/async-chat/docs/receiving_messages.md @@ -8,7 +8,7 @@ We need to: 3. parse the rest of the lines as a `login: message` ```rust -# extern crate async_std; +# extern crate tokio; # use async_std::{ # net::{TcpListener, ToSocketAddrs}, # prelude::*, @@ -77,7 +77,7 @@ We can "fix" it by waiting for the task to be joined, like this: ```rust # #![feature(async_closure)] -# extern crate async_std; +# extern crate tokio; # use async_std::{ # io::BufReader, # net::{TcpListener, TcpStream, ToSocketAddrs}, @@ -126,7 +126,7 @@ A correct way to handle client errors in this case is log them, and continue ser So let's use a helper function for this: ```rust -# extern crate async_std; +# extern crate tokio; # use async_std::{ # io, # prelude::*, diff --git a/exercise-solutions/async-chat/docs/sending_messages.md b/exercise-solutions/async-chat/docs/sending_messages.md index 95254b5a..aa00b09f 100644 --- a/exercise-solutions/async-chat/docs/sending_messages.md +++ b/exercise-solutions/async-chat/docs/sending_messages.md @@ -12,7 +12,7 @@ This task would be the point of serialization of messages. if Alice and Charley send two messages to Bob at the same time, Bob will see the messages in the same order as they arrive in the channel. ```rust -# extern crate async_std; +# extern crate tokio; # extern crate futures; # use async_std::{ # net::TcpStream, From 1a214d0f6f608b3f419255e9174f36069a1b3eb6 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 11:42:22 +0200 Subject: [PATCH 16/36] receiving_messages --- .../async-chat/docs/receiving_messages.md | 85 +++++++------------ 1 file changed, 30 insertions(+), 55 deletions(-) diff --git a/exercise-solutions/async-chat/docs/receiving_messages.md b/exercise-solutions/async-chat/docs/receiving_messages.md index 615d525c..0806308c 100644 --- a/exercise-solutions/async-chat/docs/receiving_messages.md +++ b/exercise-solutions/async-chat/docs/receiving_messages.md @@ -9,24 +9,22 @@ We need to: ```rust # extern crate tokio; -# use async_std::{ -# net::{TcpListener, ToSocketAddrs}, -# prelude::*, +# use std::{ +# collections::hash_map::{Entry, HashMap}, +# future::Future, +# }; +# use tokio::{ +# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, +# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, +# sync::{mpsc, oneshot}, # task, # }; # # type Result = std::result::Result>; # -use async_std::{ - io::BufReader, - net::TcpStream, -}; - async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let listener = TcpListener::bind(addr).await?; - let mut incoming = listener.incoming(); - while let Some(stream) = incoming.next().await { - let stream = stream?; + while let Ok((stream, _socket_addr)) = listener.accept().await { println!("Accepting from: {}", stream.peer_addr()?); let _handle = task::spawn(connection_loop(stream)); // 1 } @@ -34,23 +32,28 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { } async fn connection_loop(stream: TcpStream) -> Result<()> { - let reader = BufReader::new(&stream); // 2 - let mut lines = reader.lines(); + let reader = BufReader::new(stream); + let mut lines = reader.lines(); // 2 - let name = match lines.next().await { // 3 + // 3 + let name = match lines.next_line().await? { None => Err("peer disconnected immediately")?, - Some(line) => line?, + Some(line) => line, }; println!("name = {}", name); - while let Some(line) = lines.next().await { // 4 - let line = line?; - let (dest, msg) = match line.find(':') { // 5 + // 4 + while let Some(line) = lines.next_line().await? { + // 5 + let (dest, msg) = match line.find(':') { None => continue, - Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), + Some(idx) => (&line[..idx], line[idx + 1..].trim()), }; - let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); - let msg: String = msg.to_string(); + let dest = dest + .split(',') + .map(|name| name.trim().to_string()) + .collect::>(); + let msg = msg.to_string(); } Ok(()) } @@ -65,7 +68,7 @@ async fn connection_loop(stream: TcpStream) -> Result<()> { 3. We get the first line -- login -4. And, once again, we implement a manual async for loop. +4. And, once again, we implement a manual async loop. 5. Finally, we parse each line into a list of destination logins and the message itself. @@ -76,38 +79,13 @@ That is, `task::spawn` does not return an error immediately (it can't, it needs We can "fix" it by waiting for the task to be joined, like this: ```rust -# #![feature(async_closure)] # extern crate tokio; -# use async_std::{ -# io::BufReader, -# net::{TcpListener, TcpStream, ToSocketAddrs}, -# prelude::*, +# use tokio::{ +# net::TcpStream, # task, # }; -# # type Result = std::result::Result>; -# -# async fn connection_loop(stream: TcpStream) -> Result<()> { -# let reader = BufReader::new(&stream); // 2 -# let mut lines = reader.lines(); -# -# let name = match lines.next().await { // 3 -# None => Err("peer disconnected immediately")?, -# Some(line) => line?, -# }; -# println!("name = {}", name); -# -# while let Some(line) = lines.next().await { // 4 -# let line = line?; -# let (dest, msg) = match line.find(':') { // 5 -# None => continue, -# Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), -# }; -# let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); -# let msg: String = msg.trim().to_string(); -# } -# Ok(()) -# } +# async fn connection_loop(stream: TcpStream) -> Result<()> {} # # async move |stream| { let handle = task::spawn(connection_loop(stream)); @@ -127,11 +105,8 @@ So let's use a helper function for this: ```rust # extern crate tokio; -# use async_std::{ -# io, -# prelude::*, -# task, -# }; +# use std::future::Future; +# use tokio::task; fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> where F: Future> + Send + 'static, From 40e8d3a473a941dc065fa6c41a5d65f5dac2a6b6 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 12:23:35 +0200 Subject: [PATCH 17/36] use tokio --- exercise-solutions/async-chat/docs/all_together.md | 2 +- exercise-solutions/async-chat/docs/clean_shutdown.md | 4 ++-- .../async-chat/docs/connecting_readers_and_writers.md | 2 +- .../async-chat/docs/handling_disconnection.md | 6 +++--- exercise-solutions/async-chat/docs/implementing_a_client.md | 2 +- exercise-solutions/async-chat/docs/sending_messages.md | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/exercise-solutions/async-chat/docs/all_together.md b/exercise-solutions/async-chat/docs/all_together.md index 92c97726..231c060f 100644 --- a/exercise-solutions/async-chat/docs/all_together.md +++ b/exercise-solutions/async-chat/docs/all_together.md @@ -5,7 +5,7 @@ At this point, we only need to start the broker to get a fully-functioning (in t ```rust # extern crate tokio; # extern crate futures; -use async_std::{ +use tokio::{ io::BufReader, net::{TcpListener, TcpStream, ToSocketAddrs}, prelude::*, diff --git a/exercise-solutions/async-chat/docs/clean_shutdown.md b/exercise-solutions/async-chat/docs/clean_shutdown.md index 2384bc8d..0f17ba70 100644 --- a/exercise-solutions/async-chat/docs/clean_shutdown.md +++ b/exercise-solutions/async-chat/docs/clean_shutdown.md @@ -23,7 +23,7 @@ Let's add waiting to the server: ```rust # extern crate tokio; # extern crate futures; -# use async_std::{ +# use tokio::{ # io::{self, BufReader}, # net::{TcpListener, TcpStream, ToSocketAddrs}, # prelude::*, @@ -156,7 +156,7 @@ And to the broker: ```rust # extern crate tokio; # extern crate futures; -# use async_std::{ +# use tokio::{ # io::{self, BufReader}, # net::{TcpListener, TcpStream, ToSocketAddrs}, # prelude::*, diff --git a/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md b/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md index 760f290d..6cfdbe10 100644 --- a/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md +++ b/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md @@ -13,7 +13,7 @@ The order of events "Bob sends message to Alice" and "Alice joins" is determined ```rust # extern crate tokio; # extern crate futures; -# use async_std::{ +# use tokio::{ # net::TcpStream, # prelude::*, # task, diff --git a/exercise-solutions/async-chat/docs/handling_disconnection.md b/exercise-solutions/async-chat/docs/handling_disconnection.md index 45775751..7c7edd14 100644 --- a/exercise-solutions/async-chat/docs/handling_disconnection.md +++ b/exercise-solutions/async-chat/docs/handling_disconnection.md @@ -20,7 +20,7 @@ First, let's add a shutdown channel to the `connection_loop`: ```rust # extern crate tokio; # extern crate futures; -# use async_std::net::TcpStream; +# use tokio::net::TcpStream; # use futures::channel::mpsc; # use futures::sink::SinkExt; # use std::sync::Arc; @@ -70,7 +70,7 @@ We use the `select` macro for this purpose: ```rust # extern crate tokio; # extern crate futures; -# use async_std::{net::TcpStream, prelude::*}; +# use tokio::{net::TcpStream, prelude::*}; # use futures::channel::mpsc; use futures::{select, FutureExt}; # use std::sync::Arc; @@ -120,7 +120,7 @@ The final code looks like this: ```rust # extern crate tokio; # extern crate futures; -use async_std::{ +use tokio::{ io::BufReader, net::{TcpListener, TcpStream, ToSocketAddrs}, prelude::*, diff --git a/exercise-solutions/async-chat/docs/implementing_a_client.md b/exercise-solutions/async-chat/docs/implementing_a_client.md index 4a12c7ac..2a6f4708 100644 --- a/exercise-solutions/async-chat/docs/implementing_a_client.md +++ b/exercise-solutions/async-chat/docs/implementing_a_client.md @@ -15,7 +15,7 @@ With async, the `select!` macro is all that is needed. ```rust # extern crate tokio; # extern crate futures; -use async_std::{ +use tokio::{ io::{stdin, BufReader}, net::{TcpStream, ToSocketAddrs}, prelude::*, diff --git a/exercise-solutions/async-chat/docs/sending_messages.md b/exercise-solutions/async-chat/docs/sending_messages.md index aa00b09f..bbec88ba 100644 --- a/exercise-solutions/async-chat/docs/sending_messages.md +++ b/exercise-solutions/async-chat/docs/sending_messages.md @@ -14,7 +14,7 @@ if Alice and Charley send two messages to Bob at the same time, Bob will see the ```rust # extern crate tokio; # extern crate futures; -# use async_std::{ +# use tokio::{ # net::TcpStream, # prelude::*, # }; From 34af7d6d38ba718b732b7096fa8829ebe2d1c674 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 12:42:34 +0200 Subject: [PATCH 18/36] sending_messages --- .../async-chat/docs/sending_messages.md | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/exercise-solutions/async-chat/docs/sending_messages.md b/exercise-solutions/async-chat/docs/sending_messages.md index bbec88ba..36faa90a 100644 --- a/exercise-solutions/async-chat/docs/sending_messages.md +++ b/exercise-solutions/async-chat/docs/sending_messages.md @@ -9,36 +9,48 @@ Sending a message over a socket might require several syscalls, so two concurren As a rule of thumb, only a single task should write to each `TcpStream`. So let's create a `connection_writer_loop` task which receives messages over a channel and writes them to the socket. This task would be the point of serialization of messages. -if Alice and Charley send two messages to Bob at the same time, Bob will see the messages in the same order as they arrive in the channel. +If Alice and Charley send two messages to Bob at the same time, Bob will see the messages in the same order as they arrive in the channel. ```rust # extern crate tokio; -# extern crate futures; +# use std::{ +# collections::hash_map::{Entry, HashMap}, +# future::Future, +# }; +# # use tokio::{ -# net::TcpStream, -# prelude::*, +# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, +# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, +# sync::{mpsc, oneshot}, +# task, # }; -use futures::channel::mpsc; // 1 -use futures::sink::SinkExt; -use std::sync::Arc; - +# # type Result = std::result::Result>; +use tokio::sync::mpsc; // 1 + type Sender = mpsc::UnboundedSender; // 2 type Receiver = mpsc::UnboundedReceiver; async fn connection_writer_loop( - mut messages: Receiver, - stream: Arc, // 3 + messages: &mut Receiver, + stream: &mut OwnedWriteHalf, ) -> Result<()> { - let mut stream = &*stream; - while let Some(msg) = messages.next().await { + while let Some(msg) = messages.recv().await { stream.write_all(msg.as_bytes()).await?; } Ok(()) } ``` -1. We will use channels from the `futures` crate. +1. We will use `mpsc` channels from `tokio`. 2. For simplicity, we will use `unbounded` channels, and won't be discussing backpressure in this tutorial. -3. As `connection_loop` and `connection_writer_loop` share the same `TcpStream`, we need to put it into an `Arc`. - Note that because `client` only reads from the stream and `connection_writer_loop` only writes to the stream, we don't get a race here. +3. As `connection_loop` and `connection_writer_loop` share the same `TcpStream`, we need to split it into a reader and a writer: + + ```rust + # extern crate tokio; + # use tokio::net::TcpStream; + # let stream: TcpStream = unimplemented!(); + # + use tokio::net::tcp; + let (reader, writer): (tcp::OwnedReadHalf, tcp::OwnedWriteHalf) = stream.into_split(); + ``` From 108c0213fa47632d1792ab099a32fd29f9edc45f Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 13:27:50 +0200 Subject: [PATCH 19/36] connecting_readers_and_writers --- .../docs/connecting_readers_and_writers.md | 65 ++++++++----------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md b/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md index 6cfdbe10..db7746b2 100644 --- a/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md +++ b/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md @@ -12,16 +12,16 @@ The order of events "Bob sends message to Alice" and "Alice joins" is determined ```rust # extern crate tokio; -# extern crate futures; +# use std::{ +# collections::hash_map::{Entry, HashMap}, +# future::Future, +# }; # use tokio::{ -# net::TcpStream, -# prelude::*, +# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, +# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, +# sync::{mpsc, oneshot}, # task, # }; -# use futures::channel::mpsc; -# use futures::sink::SinkExt; -# use std::sync::Arc; -# # type Result = std::result::Result>; # type Sender = mpsc::UnboundedSender; # type Receiver = mpsc::UnboundedReceiver; @@ -29,24 +29,12 @@ The order of events "Bob sends message to Alice" and "Alice joins" is determined # async fn connection_writer_loop( # mut messages: Receiver, # stream: Arc, -# ) -> Result<()> { -# let mut stream = &*stream; -# while let Some(msg) = messages.next().await { -# stream.write_all(msg.as_bytes()).await?; -# } -# Ok(()) -# } +# ) -> Result<()> {} # # fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> # where # F: Future> + Send + 'static, -# { -# task::spawn(async move { -# if let Err(e) = fut.await { -# eprintln!("{}", e) -# } -# }) -# } +# {} # use std::collections::hash_map::{Entry, HashMap}; @@ -54,7 +42,7 @@ use std::collections::hash_map::{Entry, HashMap}; enum Event { // 1 NewPeer { name: String, - stream: Arc, + stream: OwnedWriteHalf, }, Message { from: String, @@ -63,38 +51,37 @@ enum Event { // 1 }, } -async fn broker_loop(mut events: Receiver) -> Result<()> { +async fn broker_loop(mut events: Receiver) { let mut peers: HashMap> = HashMap::new(); // 2 - while let Some(event) = events.next().await { + while let Some(event) = events.recv().await { match event { - Event::Message { from, to, msg } => { // 3 + Event::Message { from, to, msg } => { // 3 for addr in to { if let Some(peer) = peers.get_mut(&addr) { - let msg = format!("from {}: {}\n", from, msg); - peer.send(msg).await? + let msg = format!("from {from}: {msg}\n"); + peer.send(msg).unwrap(); } } } - Event::NewPeer { name, stream } => { - match peers.entry(name) { - Entry::Occupied(..) => (), - Entry::Vacant(entry) => { - let (client_sender, client_receiver) = mpsc::unbounded(); - entry.insert(client_sender); // 4 - spawn_and_log_error(connection_writer_loop(client_receiver, stream)); // 5 - } + Event::NewPeer { name, mut stream } => match peers.entry(name.clone()) { + Entry::Occupied(..) => (), + Entry::Vacant(entry) => { + let (client_sender, mut client_receiver) = mpsc::unbounded_channel(); + entry.insert(client_sender); // 4 + spawn_and_log_error(async move { + connection_writer_loop(&mut client_receiver, &mut stream).await + }); // 5 } - } + }, } } - Ok(()) } ``` 1. The broker task should handle two types of events: a message or an arrival of a new peer. 2. The internal state of the broker is a `HashMap`. - Note how we don't need a `Mutex` here and can confidently say, at each iteration of the broker's loop, what is the current set of peers -3. To handle a message, we send it over a channel to each destination + Note how we don't need a `Mutex` here and can confidently say, at each iteration of the broker's loop, what is the current set of peers. +3. To handle a message, we send it over a channel to each destination. 4. To handle a new peer, we first register it in the peer's map ... 5. ... and then spawn a dedicated task to actually write the messages to the socket. From 4d0b68a9358fff55d0ac267e274ae6a3a8403e47 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 15:27:52 +0200 Subject: [PATCH 20/36] all_together --- .../async-chat/docs/all_together.md | 144 +++++++++--------- 1 file changed, 76 insertions(+), 68 deletions(-) diff --git a/exercise-solutions/async-chat/docs/all_together.md b/exercise-solutions/async-chat/docs/all_together.md index 231c060f..a69018fc 100644 --- a/exercise-solutions/async-chat/docs/all_together.md +++ b/exercise-solutions/async-chat/docs/all_together.md @@ -4,90 +4,88 @@ At this point, we only need to start the broker to get a fully-functioning (in t ```rust # extern crate tokio; -# extern crate futures; +use std::{ + collections::hash_map::{Entry, HashMap}, + future::Future, +}; + use tokio::{ - io::BufReader, - net::{TcpListener, TcpStream, ToSocketAddrs}, - prelude::*, + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, + sync::mpsc, task, }; -use futures::channel::mpsc; -use futures::sink::SinkExt; -use std::{ - collections::hash_map::{HashMap, Entry}, - sync::Arc, -}; type Result = std::result::Result>; type Sender = mpsc::UnboundedSender; type Receiver = mpsc::UnboundedReceiver; -// main -fn run() -> Result<()> { - task::block_on(accept_loop("127.0.0.1:8080")) -} - -fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> -where - F: Future> + Send + 'static, -{ - task::spawn(async move { - if let Err(e) = fut.await { - eprintln!("{}", e) - } - }) +#[tokio::main] +async fn main() -> Result<()> { + accept_loop("127.0.0.1:8080").await } async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let listener = TcpListener::bind(addr).await?; - let (broker_sender, broker_receiver) = mpsc::unbounded(); // 1 - let _broker_handle = task::spawn(broker_loop(broker_receiver)); - let mut incoming = listener.incoming(); - while let Some(stream) = incoming.next().await { - let stream = stream?; + let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); // 1 + let _broker = task::spawn(broker_loop(broker_receiver)); + + while let Ok((stream, _socket_addr)) = listener.accept().await { println!("Accepting from: {}", stream.peer_addr()?); spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); } Ok(()) } -async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result<()> { - let stream = Arc::new(stream); // 2 - let reader = BufReader::new(&*stream); +async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { + let (reader, writer) = stream.into_split(); // 2 + let reader = BufReader::new(reader); let mut lines = reader.lines(); - let name = match lines.next().await { - None => Err("peer disconnected immediately")?, - Some(line) => line?, + let name = match lines.next_line().await { + Ok(Some(line)) => line, + Ok(None) => return Err("peer disconnected immediately".into()), + Err(e) => return Err(Box::new(e)), }; - broker.send(Event::NewPeer { name: name.clone(), stream: Arc::clone(&stream) }).await // 3 - .unwrap(); - while let Some(line) = lines.next().await { - let line = line?; + println!("user {} connected", name); + + broker + .send(Event::NewPeer { + name: name.clone(), + stream: writer, + }) + .unwrap(); // 3 + + while let Ok(Some(line)) = lines.next_line().await { let (dest, msg) = match line.find(':') { None => continue, - Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), + Some(idx) => (&line[..idx], line[idx + 1..].trim()), }; - let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); - let msg: String = msg.to_string(); - - broker.send(Event::Message { // 4 - from: name.clone(), - to: dest, - msg, - }).await.unwrap(); + let dest: Vec = dest + .split(',') + .map(|name| name.trim().to_string()) + .collect(); + let msg: String = msg.trim().to_string(); + + broker + .send(Event::Message { // 4 + from: name.clone(), + to: dest, + msg, + }) + .unwrap(); } + Ok(()) } async fn connection_writer_loop( - mut messages: Receiver, - stream: Arc, + messages: &mut Receiver, + stream: &mut OwnedWriteHalf, ) -> Result<()> { - let mut stream = &*stream; - while let Some(msg) = messages.next().await { + while let Some(msg) = messages.recv().await { stream.write_all(msg.as_bytes()).await?; } Ok(()) @@ -97,7 +95,7 @@ async fn connection_writer_loop( enum Event { NewPeer { name: String, - stream: Arc, + stream: OwnedWriteHalf, }, Message { from: String, @@ -106,37 +104,47 @@ enum Event { }, } -async fn broker_loop(mut events: Receiver) -> Result<()> { +async fn broker_loop(mut events: Receiver) { let mut peers: HashMap> = HashMap::new(); - while let Some(event) = events.next().await { + while let Some(event) = events.recv().await { match event { Event::Message { from, to, msg } => { for addr in to { if let Some(peer) = peers.get_mut(&addr) { - let msg = format!("from {}: {}\n", from, msg); - peer.send(msg).await? + let msg = format!("from {from}: {msg}\n"); + peer.send(msg).unwrap(); } } } - Event::NewPeer { name, stream} => { - match peers.entry(name) { - Entry::Occupied(..) => (), - Entry::Vacant(entry) => { - let (client_sender, client_receiver) = mpsc::unbounded(); - entry.insert(client_sender); - spawn_and_log_error(connection_writer_loop(client_receiver, stream)); - } + Event::NewPeer { name, mut stream } => match peers.entry(name.clone()) { + Entry::Occupied(..) => (), + Entry::Vacant(entry) => { + let (client_sender, mut client_receiver) = mpsc::unbounded_channel(); + entry.insert(client_sender); + spawn_and_log_error(async move { + connection_writer_loop(&mut client_receiver, &mut stream).await + }); } - } + }, } } - Ok(()) +} + +fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +where + F: Future> + Send + 'static, +{ + task::spawn(async move { + if let Err(e) = fut.await { + eprintln!("{}", e) + } + }) } ``` 1. Inside the `accept_loop`, we create the broker's channel and `task`. -2. Inside `connection_loop`, we need to wrap `TcpStream` into an `Arc`, to be able to share it with the `connection_writer_loop`. +2. Inside `connection_loop`, we need to split the `TcpStream`, to be able to share it with the `connection_writer_loop`. 3. On login, we notify the broker. Note that we `.unwrap` on send: broker should outlive all the clients and if that's not the case the broker probably panicked, so we can escalate the panic as well. 4. Similarly, we forward parsed messages to the broker, assuming that it is alive. From 09b58f152c313762f0b2c0eaac160ab03aba1160 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 16:14:40 +0200 Subject: [PATCH 21/36] Implementing a client --- .../async-chat/docs/implementing_a_client.md | 44 +++++++++---------- exercise-solutions/async-chat/src/client.rs | 9 ++-- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/exercise-solutions/async-chat/docs/implementing_a_client.md b/exercise-solutions/async-chat/docs/implementing_a_client.md index 2a6f4708..82c3b1c3 100644 --- a/exercise-solutions/async-chat/docs/implementing_a_client.md +++ b/exercise-solutions/async-chat/docs/implementing_a_client.md @@ -14,43 +14,41 @@ With async, the `select!` macro is all that is needed. ```rust # extern crate tokio; -# extern crate futures; use tokio::{ - io::{stdin, BufReader}, + io::{stdin, AsyncBufReadExt, AsyncWriteExt, BufReader}, net::{TcpStream, ToSocketAddrs}, - prelude::*, - task, }; -use futures::{select, FutureExt}; type Result = std::result::Result>; -// main -fn run() -> Result<()> { - task::block_on(try_run("127.0.0.1:8080")) +#[tokio::main] +async fn main() -> Result<()> { + try_main("127.0.0.1:8080").await } -async fn try_run(addr: impl ToSocketAddrs) -> Result<()> { +async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { let stream = TcpStream::connect(addr).await?; - let (reader, mut writer) = (&stream, &stream); // 1 - let mut lines_from_server = BufReader::new(reader).lines().fuse(); // 2 - let mut lines_from_stdin = BufReader::new(stdin()).lines().fuse(); // 2 + let (reader, mut writer) = stream.into_split(); + + let mut lines_from_server = BufReader::new(reader).lines(); // 2 + let mut lines_from_stdin = BufReader::new(stdin()).lines(); // 2 + loop { - select! { // 3 - line = lines_from_server.next().fuse() => match line { - Some(line) => { - let line = line?; + tokio::select! { // 3 + line = lines_from_server.next_line() => match line { + Ok(Some(line)) => { println!("{}", line); }, - None => break, + Ok(None) => break, + Err(e) => eprintln!("Error {:?}:", e), }, - line = lines_from_stdin.next().fuse() => match line { - Some(line) => { - let line = line?; + line = lines_from_stdin.next_line() => match line { + Ok(Some(line)) => { writer.write_all(line.as_bytes()).await?; writer.write_all(b"\n").await?; - } - None => break, + }, + Ok(None) => break, + Err(e) => eprintln!("Error {:?}:", e), } } } @@ -58,6 +56,6 @@ async fn try_run(addr: impl ToSocketAddrs) -> Result<()> { } ``` -1. Here we split `TcpStream` into read and write halves: there's `impl AsyncRead for &'_ TcpStream`, just like the one in std. +1. Here we split `TcpStream` into read and write halves. 2. We create a stream of lines for both the socket and stdin. 3. In the main select loop, we print the lines we receive from the server and send the lines we read from the console. diff --git a/exercise-solutions/async-chat/src/client.rs b/exercise-solutions/async-chat/src/client.rs index 3fb5b098..b1bb8ead 100644 --- a/exercise-solutions/async-chat/src/client.rs +++ b/exercise-solutions/async-chat/src/client.rs @@ -12,12 +12,9 @@ pub(crate) async fn main() -> Result<()> { async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { let stream = TcpStream::connect(addr).await?; - let (reader, mut writer) = tokio::io::split(stream); - let reader = BufReader::new(reader); - let mut lines_from_server = reader.lines(); - - let stdin = BufReader::new(stdin()); - let mut lines_from_stdin = stdin.lines(); + let (reader, mut writer) = stream.into_split(); + let mut lines_from_server = BufReader::new(reader).lines(); + let mut lines_from_stdin = BufReader::new(stdin()).lines(); loop { tokio::select! { line = lines_from_server.next_line() => match line { From 734e5a13381a0ca4cfe8a62166dac1267df9a9ac Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 16:17:09 +0200 Subject: [PATCH 22/36] handling_disconnection --- exercise-solutions/async-chat/docs/SUMMARY.md | 1 + .../async-chat/docs/final_server_code.md | 191 ++++++++++++++ .../async-chat/docs/handling_disconnection.md | 237 ++---------------- 3 files changed, 209 insertions(+), 220 deletions(-) create mode 100644 exercise-solutions/async-chat/docs/final_server_code.md diff --git a/exercise-solutions/async-chat/docs/SUMMARY.md b/exercise-solutions/async-chat/docs/SUMMARY.md index 190c69de..9c5a7112 100644 --- a/exercise-solutions/async-chat/docs/SUMMARY.md +++ b/exercise-solutions/async-chat/docs/SUMMARY.md @@ -9,4 +9,5 @@ - [All Together](./all_together.md) - [Clean Shutdown](./clean_shutdown.md) - [Handling Disconnection](./handling_disconnection.md) + - [Final Server Code](./final_server_code.md) - [Implementing a Client](./implementing_a_client.md) diff --git a/exercise-solutions/async-chat/docs/final_server_code.md b/exercise-solutions/async-chat/docs/final_server_code.md new file mode 100644 index 00000000..3076e38e --- /dev/null +++ b/exercise-solutions/async-chat/docs/final_server_code.md @@ -0,0 +1,191 @@ +# Final Server Code + +The final code looks like this: + +```rust +# extern crate tokio; +use std::{ + collections::hash_map::{Entry, HashMap}, + future::Future, +}; + +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, + sync::{mpsc, oneshot}, + task, +}; + +type Result = std::result::Result>; +type Sender = mpsc::UnboundedSender; +type Receiver = mpsc::UnboundedReceiver; + +#[tokio::main] +pub(crate) async fn main() -> Result<()> { + accept_loop("127.0.0.1:8080").await +} + +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + let listener = TcpListener::bind(addr).await?; + + let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); + let broker = task::spawn(broker_loop(broker_receiver)); + + while let Ok((stream, _socket_addr)) = listener.accept().await { + println!("Accepting from: {}", stream.peer_addr()?); + spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); + } + drop(broker_sender); + broker.await?; + Ok(()) +} + +async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { + let (reader, writer) = stream.into_split(); + let reader = BufReader::new(reader); + let mut lines = reader.lines(); + + let name = match lines.next_line().await { + Ok(Some(line)) => line, + Ok(None) => return Err("peer disconnected immediately".into()), + Err(e) => return Err(Box::new(e)), + }; + + println!("user {} connected", name); + + let (_shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); + broker + .send(Event::NewPeer { + name: name.clone(), + stream: writer, + shutdown: shutdown_receiver, + }) + .unwrap(); + + while let Ok(Some(line)) = lines.next_line().await { + let (dest, msg) = match line.find(':') { + None => continue, + Some(idx) => (&line[..idx], line[idx + 1..].trim()), + }; + let dest: Vec = dest + .split(',') + .map(|name| name.trim().to_string()) + .collect(); + let msg: String = msg.trim().to_string(); + + broker + .send(Event::Message { + from: name.clone(), + to: dest, + msg, + }) + .unwrap(); + } + + Ok(()) +} + +async fn connection_writer_loop( + messages: &mut Receiver, + stream: &mut OwnedWriteHalf, + mut shutdown: oneshot::Receiver<()>, +) -> Result<()> { + loop { + tokio::select! { + msg = messages.recv() => match msg { + Some(msg) => stream.write_all(msg.as_bytes()).await?, + None => break, + }, + _ = &mut shutdown => break + } + } + Ok(()) +} + +#[derive(Debug)] +enum Event { + NewPeer { + name: String, + stream: OwnedWriteHalf, + shutdown: oneshot::Receiver<()>, + }, + Message { + from: String, + to: Vec, + msg: String, + }, +} + +async fn broker_loop(mut events: Receiver) { + let (disconnect_sender, mut disconnect_receiver) = + mpsc::unbounded_channel::<(String, Receiver)>(); // 1 + let mut peers: HashMap> = HashMap::new(); + + loop { + let event = tokio::select! { + event = events.recv() => match event { + None => break, // 2 + Some(event) => event, + }, + disconnect = disconnect_receiver.recv() => { + let (name, _pending_messages) = disconnect.unwrap(); // 3 + assert!(peers.remove(&name).is_some()); + continue; + }, + }; + match event { + Event::Message { from, to, msg } => { + for addr in to { + if let Some(peer) = peers.get_mut(&addr) { + let msg = format!("from {}: {}\n", from, msg); + peer.send(msg).unwrap(); // 6 + } + } + } + Event::NewPeer { + name, + mut stream, + shutdown, + } => match peers.entry(name.clone()) { + Entry::Occupied(..) => (), + Entry::Vacant(entry) => { + let (client_sender, mut client_receiver) = mpsc::unbounded_channel(); + entry.insert(client_sender); + let disconnect_sender = disconnect_sender.clone(); + spawn_and_log_error(async move { + let res = + connection_writer_loop(&mut client_receiver, &mut stream, shutdown) + .await; + disconnect_sender.send((name, client_receiver)).unwrap(); // 4 + res + }); + } + }, + } + } + drop(peers); // 5 + drop(disconnect_sender); // 6 + while let Some((_name, _pending_messages)) = disconnect_receiver.recv().await {} +} + +fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +where + F: Future> + Send + 'static, +{ + task::spawn(async move { + if let Err(e) = fut.await { + eprintln!("{}", e) + } + }) +} +``` + +1. In the broker, we create a channel to reap disconnected peers and their undelivered messages. +2. The broker's main loop exits when the input events channel is exhausted (that is, when all readers exit). +3. Because broker itself holds a `disconnect_sender`, we know that the disconnections channel can't be fully drained in the main loop. +4. We send peer's name and pending messages to the disconnections channel in both the happy and the not-so-happy path. + Again, we can safely unwrap because the broker outlives writers. +5. We drop `peers` map to close writers' messages channel and shut down the writers for sure. + It is not strictly necessary in the current setup, where the broker waits for readers' shutdown anyway. + However, if we add a server-initiated shutdown (for example, kbd:[ctrl+c] handling), this will be a way for the broker to shutdown the writers. +6. Finally, we close and drain the disconnections channel. diff --git a/exercise-solutions/async-chat/docs/handling_disconnection.md b/exercise-solutions/async-chat/docs/handling_disconnection.md index 7c7edd14..f8cab8d4 100644 --- a/exercise-solutions/async-chat/docs/handling_disconnection.md +++ b/exercise-solutions/async-chat/docs/handling_disconnection.md @@ -29,15 +29,13 @@ First, let's add a shutdown channel to the `connection_loop`: # type Sender = mpsc::UnboundedSender; # type Receiver = mpsc::UnboundedReceiver; # -#[derive(Debug)] -enum Void {} // 1 #[derive(Debug)] enum Event { NewPeer { name: String, - stream: Arc, - shutdown: Receiver, // 2 + stream: OwnedWriteHalf, + shutdown: oneshot::Receiver<()>, }, Message { from: String, @@ -49,18 +47,20 @@ enum Event { async fn connection_loop(mut broker: Sender, stream: Arc) -> Result<()> { // ... # let name: String = unimplemented!(); - let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded::(); // 3 - broker.send(Event::NewPeer { - name: name.clone(), - stream: Arc::clone(&stream), - shutdown: shutdown_receiver, - }).await.unwrap(); + let (_shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); + broker + .send(Event::NewPeer { + name: name.clone(), + stream: writer, + shutdown: shutdown_receiver, + }) + .unwrap(); // ... # unimplemented!() } ``` -1. To enforce that no messages are sent along the shutdown channel, we use an uninhabited type. +1. To enforce that no messages are sent along the shutdown channel, we use a oneshot channel. 2. We pass the shutdown channel to the writer task. 3. In the reader, we create a `_shutdown_sender` whose only purpose is to get dropped. @@ -77,27 +77,19 @@ use futures::{select, FutureExt}; # type Receiver = mpsc::UnboundedReceiver; # type Result = std::result::Result>; # type Sender = mpsc::UnboundedSender; -# #[derive(Debug)] -# enum Void {} // 1 async fn connection_writer_loop( messages: &mut Receiver, - stream: Arc, - shutdown: Receiver, // 1 + stream: &mut OwnedWriteHalf, + mut shutdown: oneshot::Receiver<()>, // 1 ) -> Result<()> { - let mut stream = &*stream; - let mut messages = messages.fuse(); - let mut shutdown = shutdown.fuse(); loop { // 2 - select! { - msg = messages.next().fuse() => match msg { // 3 + tokio::select! { + msg = messages.recv() => match msg { Some(msg) => stream.write_all(msg.as_bytes()).await?, None => break, }, - void = shutdown.next().fuse() => match void { - Some(void) => match void {}, // 4 - None => break, - } + _ = &mut shutdown => break } } Ok(()) @@ -106,203 +98,8 @@ async fn connection_writer_loop( 1. We add shutdown channel as an argument. 2. Because of `select`, we can't use a `while let` loop, so we desugar it further into a `loop`. -3. Function fuse() is used to turn any `Stream` into a `FusedStream`. This is used for fusing a stream such that poll_next will never again be called once it has finished. -4. In the shutdown case we use `match void {}` as a statically-checked `unreachable!()`. +3. In the shutdown case break the loop. Another problem is that between the moment we detect disconnection in `connection_writer_loop` and the moment when we actually remove the peer from the `peers` map, new messages might be pushed into the peer's channel. To not lose these messages completely, we'll return the messages channel back to the broker. This also allows us to establish a useful invariant that the message channel strictly outlives the peer in the `peers` map, and makes the broker itself infallible. - -## Final Code - -The final code looks like this: - -```rust -# extern crate tokio; -# extern crate futures; -use tokio::{ - io::BufReader, - net::{TcpListener, TcpStream, ToSocketAddrs}, - prelude::*, - task, -}; -use futures::channel::mpsc; -use futures::sink::SinkExt; -use futures::{select, FutureExt}; -use std::{ - collections::hash_map::{Entry, HashMap}, - future::Future, - sync::Arc, -}; - -type Result = std::result::Result>; -type Sender = mpsc::UnboundedSender; -type Receiver = mpsc::UnboundedReceiver; - -#[derive(Debug)] -enum Void {} - -// main -fn run() -> Result<()> { - task::block_on(accept_loop("127.0.0.1:8080")) -} - -async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { - let listener = TcpListener::bind(addr).await?; - let (broker_sender, broker_receiver) = mpsc::unbounded(); - let broker_handle = task::spawn(broker_loop(broker_receiver)); - let mut incoming = listener.incoming(); - while let Some(stream) = incoming.next().await { - let stream = stream?; - println!("Accepting from: {}", stream.peer_addr()?); - spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); - } - drop(broker_sender); - broker_handle.await; - Ok(()) -} - -async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result<()> { - let stream = Arc::new(stream); - let reader = BufReader::new(&*stream); - let mut lines = reader.lines(); - - let name = match lines.next().await { - None => Err("peer disconnected immediately")?, - Some(line) => line?, - }; - let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded::(); - broker.send(Event::NewPeer { - name: name.clone(), - stream: Arc::clone(&stream), - shutdown: shutdown_receiver, - }).await.unwrap(); - - while let Some(line) = lines.next().await { - let line = line?; - let (dest, msg) = match line.find(':') { - None => continue, - Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), - }; - let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); - let msg: String = msg.trim().to_string(); - - broker.send(Event::Message { - from: name.clone(), - to: dest, - msg, - }).await.unwrap(); - } - - Ok(()) -} - -async fn connection_writer_loop( - messages: &mut Receiver, - stream: Arc, - shutdown: Receiver, -) -> Result<()> { - let mut stream = &*stream; - let mut messages = messages.fuse(); - let mut shutdown = shutdown.fuse(); - loop { - select! { - msg = messages.next().fuse() => match msg { - Some(msg) => stream.write_all(msg.as_bytes()).await?, - None => break, - }, - void = shutdown.next().fuse() => match void { - Some(void) => match void {}, - None => break, - } - } - } - Ok(()) -} - -#[derive(Debug)] -enum Event { - NewPeer { - name: String, - stream: Arc, - shutdown: Receiver, - }, - Message { - from: String, - to: Vec, - msg: String, - }, -} - -async fn broker_loop(events: Receiver) { - let (disconnect_sender, mut disconnect_receiver) = // 1 - mpsc::unbounded::<(String, Receiver)>(); - let mut peers: HashMap> = HashMap::new(); - let mut events = events.fuse(); - loop { - let event = select! { - event = events.next().fuse() => match event { - None => break, // 2 - Some(event) => event, - }, - disconnect = disconnect_receiver.next().fuse() => { - let (name, _pending_messages) = disconnect.unwrap(); // 3 - assert!(peers.remove(&name).is_some()); - continue; - }, - }; - match event { - Event::Message { from, to, msg } => { - for addr in to { - if let Some(peer) = peers.get_mut(&addr) { - let msg = format!("from {}: {}\n", from, msg); - peer.send(msg).await - .unwrap() // 6 - } - } - } - Event::NewPeer { name, stream, shutdown } => { - match peers.entry(name.clone()) { - Entry::Occupied(..) => (), - Entry::Vacant(entry) => { - let (client_sender, mut client_receiver) = mpsc::unbounded(); - entry.insert(client_sender); - let mut disconnect_sender = disconnect_sender.clone(); - spawn_and_log_error(async move { - let res = connection_writer_loop(&mut client_receiver, stream, shutdown).await; - disconnect_sender.send((name, client_receiver)).await // 4 - .unwrap(); - res - }); - } - } - } - } - } - drop(peers); // 5 - drop(disconnect_sender); // 6 - while let Some((_name, _pending_messages)) = disconnect_receiver.next().await { - } -} - -fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> -where - F: Future> + Send + 'static, -{ - task::spawn(async move { - if let Err(e) = fut.await { - eprintln!("{}", e) - } - }) -} -``` - -1. In the broker, we create a channel to reap disconnected peers and their undelivered messages. -2. The broker's main loop exits when the input events channel is exhausted (that is, when all readers exit). -3. Because broker itself holds a `disconnect_sender`, we know that the disconnections channel can't be fully drained in the main loop. -4. We send peer's name and pending messages to the disconnections channel in both the happy and the not-so-happy path. - Again, we can safely unwrap because the broker outlives writers. -5. We drop `peers` map to close writers' messages channel and shut down the writers for sure. - It is not strictly necessary in the current setup, where the broker waits for readers' shutdown anyway. - However, if we add a server-initiated shutdown (for example, kbd:[ctrl+c] handling), this will be a way for the broker to shutdown the writers. -6. Finally, we close and drain the disconnections channel. From bea1413291c91a71d2ceca9ae8f57afb39074bbd Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 16:27:51 +0200 Subject: [PATCH 23/36] clean_shutdown --- .../async-chat/docs/clean_shutdown.md | 268 +++++------------- 1 file changed, 73 insertions(+), 195 deletions(-) diff --git a/exercise-solutions/async-chat/docs/clean_shutdown.md b/exercise-solutions/async-chat/docs/clean_shutdown.md index 0f17ba70..2fae9412 100644 --- a/exercise-solutions/async-chat/docs/clean_shutdown.md +++ b/exercise-solutions/async-chat/docs/clean_shutdown.md @@ -21,132 +21,50 @@ However, we never wait for broker and writers, which might cause some messages t Let's add waiting to the server: ```rust -# extern crate tokio; -# extern crate futures; -# use tokio::{ -# io::{self, BufReader}, -# net::{TcpListener, TcpStream, ToSocketAddrs}, -# prelude::*, -# task, -# }; -# use futures::channel::mpsc; -# use futures::sink::SinkExt; -# use std::{ -# collections::hash_map::{HashMap, Entry}, -# sync::Arc, -# }; -# -# type Result = std::result::Result>; -# type Sender = mpsc::UnboundedSender; -# type Receiver = mpsc::UnboundedReceiver; -# -# fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> -# where -# F: Future> + Send + 'static, -# { -# task::spawn(async move { -# if let Err(e) = fut.await { -# eprintln!("{}", e) -# } -# }) -# } -# -# -# async fn connection_loop(mut broker: Sender, stream: TcpStream) -> Result<()> { -# let stream = Arc::new(stream); // 2 -# let reader = BufReader::new(&*stream); -# let mut lines = reader.lines(); -# -# let name = match lines.next().await { -# None => Err("peer disconnected immediately")?, -# Some(line) => line?, -# }; -# broker.send(Event::NewPeer { name: name.clone(), stream: Arc::clone(&stream) }).await // 3 -# .unwrap(); -# -# while let Some(line) = lines.next().await { -# let line = line?; -# let (dest, msg) = match line.find(':') { -# None => continue, -# Some(idx) => (&line[..idx], line[idx + 1 ..].trim()), -# }; -# let dest: Vec = dest.split(',').map(|name| name.trim().to_string()).collect(); -# let msg: String = msg.trim().to_string(); -# -# broker.send(Event::Message { // 4 -# from: name.clone(), -# to: dest, -# msg, -# }).await.unwrap(); -# } -# Ok(()) -# } -# -# async fn connection_writer_loop( -# mut messages: Receiver, -# stream: Arc, -# ) -> Result<()> { -# let mut stream = &*stream; -# while let Some(msg) = messages.next().await { -# stream.write_all(msg.as_bytes()).await?; -# } -# Ok(()) -# } -# -# #[derive(Debug)] -# enum Event { -# NewPeer { -# name: String, -# stream: Arc, -# }, -# Message { -# from: String, -# to: Vec, -# msg: String, -# }, -# } -# -# async fn broker_loop(mut events: Receiver) -> Result<()> { -# let mut peers: HashMap> = HashMap::new(); -# -# while let Some(event) = events.next().await { -# match event { -# Event::Message { from, to, msg } => { -# for addr in to { -# if let Some(peer) = peers.get_mut(&addr) { -# let msg = format!("from {}: {}\n", from, msg); -# peer.send(msg).await? -# } -# } -# } -# Event::NewPeer { name, stream} => { -# match peers.entry(name) { -# Entry::Occupied(..) => (), -# Entry::Vacant(entry) => { -# let (client_sender, client_receiver) = mpsc::unbounded(); -# entry.insert(client_sender); // 4 -# spawn_and_log_error(connection_writer_loop(client_receiver, stream)); // 5 -# } -# } -# } -# } -# } -# Ok(()) -# } -# async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let listener = TcpListener::bind(addr).await?; - let (broker_sender, broker_receiver) = mpsc::unbounded(); - let broker_handle = task::spawn(broker_loop(broker_receiver)); - let mut incoming = listener.incoming(); - while let Some(stream) = incoming.next().await { - let stream = stream?; + let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); + let broker = task::spawn(broker_loop(broker_receiver)); + + while let Ok((stream, _socket_addr)) = listener.accept().await { println!("Accepting from: {}", stream.peer_addr()?); spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); } drop(broker_sender); // 1 - broker_handle.await?; // 5 + broker.await?; // 5 + Ok(()) +} +``` + +Event + connection_loop: + +```rust +#[derive(Debug)] +enum Event { + NewPeer {unchanged + name: String, + stream: OwnedWriteHalf, + shutdown: oneshot::Receiver<()>, + }, + Message { /* unchanged */ }, +} + +async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { + // ... + let (_shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); + broker + .send(Event::NewPeer { + name: name.clone(), + stream: writer, + shutdown: shutdown_receiver, + }) + .unwrap(); + + while let Ok(Some(line)) = lines.next_line().await { + // ... + } + Ok(()) } ``` @@ -154,89 +72,49 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { And to the broker: ```rust -# extern crate tokio; -# extern crate futures; -# use tokio::{ -# io::{self, BufReader}, -# net::{TcpListener, TcpStream, ToSocketAddrs}, -# prelude::*, -# task, -# }; -# use futures::channel::mpsc; -# use futures::sink::SinkExt; -# use std::{ -# collections::hash_map::{HashMap, Entry}, -# sync::Arc, -# }; -# -# type Result = std::result::Result>; -# type Sender = mpsc::UnboundedSender; -# type Receiver = mpsc::UnboundedReceiver; -# -# async fn connection_writer_loop( -# mut messages: Receiver, -# stream: Arc, -# ) -> Result<()> { -# let mut stream = &*stream; -# while let Some(msg) = messages.next().await { -# stream.write_all(msg.as_bytes()).await?; -# } -# Ok(()) -# } -# -# fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> -# where -# F: Future> + Send + 'static, -# { -# task::spawn(async move { -# if let Err(e) = fut.await { -# eprintln!("{}", e) -# } -# }) -# } -# -# #[derive(Debug)] -# enum Event { -# NewPeer { -# name: String, -# stream: Arc, -# }, -# Message { -# from: String, -# to: Vec, -# msg: String, -# }, -# } -# -async fn broker_loop(mut events: Receiver) -> Result<()> { - let mut writers = Vec::new(); +async fn broker_loop(mut events: Receiver) { let mut peers: HashMap> = HashMap::new(); - while let Some(event) = events.next().await { // 2 + + while let Some(event) = events.recv().await { match event { Event::Message { from, to, msg } => { - for addr in to { - if let Some(peer) = peers.get_mut(&addr) { - let msg = format!("from {}: {}\n", from, msg); - peer.send(msg).await? - } - } + // ... } - Event::NewPeer { name, stream} => { - match peers.entry(name) { - Entry::Occupied(..) => (), - Entry::Vacant(entry) => { - let (client_sender, client_receiver) = mpsc::unbounded(); - entry.insert(client_sender); - let handle = spawn_and_log_error(connection_writer_loop(client_receiver, stream)); - writers.push(handle); // 4 - } + Event::NewPeer { + name, + mut stream, + shutdown, + } => match peers.entry(name.clone()) { + Entry::Occupied(..) => (), + Entry::Vacant(entry) => { + let (client_sender, mut client_receiver) = mpsc::unbounded_channel(); + entry.insert(client_sender); + spawn_and_log_error(async move { + connection_writer_loop(&mut client_receiver, &mut stream, shutdown).await + }); } - } + }, } } - drop(peers); // 3 - for writer in writers { // 4 - writer.await; +} +``` + +connection_writer_loop: + +```rust +async fn connection_writer_loop( + messages: &mut Receiver, + stream: &mut OwnedWriteHalf, + mut shutdown: oneshot::Receiver<()>, +) -> Result<()> { + loop { + tokio::select! { + msg = messages.recv() => match msg { + Some(msg) => stream.write_all(msg.as_bytes()).await?, + None => break, + }, + _ = &mut shutdown => break + } } Ok(()) } From 1eb6e5d1dadf761894b531a56bb4c621dea392ee Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 16:29:06 +0200 Subject: [PATCH 24/36] Use split_once --- exercise-solutions/async-chat/src/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index 3f708883..be50c4e5 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -57,9 +57,9 @@ async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> .unwrap(); while let Ok(Some(line)) = lines.next_line().await { - let (dest, msg) = match line.find(':') { + let (dest, msg) = match line.split_once(':') { None => continue, - Some(idx) => (&line[..idx], line[idx + 1..].trim()), + Some((dest, msg)) => (dest, msg.trim()), }; let dest: Vec = dest .split(',') From 1ca872fc4a6c339af3482b99418b99b7e733252c Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 17:03:23 +0200 Subject: [PATCH 25/36] Fix `mdbook test -L ../target/debug/ -L ../target/debug/deps/` --- exercise-solutions/async-chat/book.toml | 3 + .../async-chat/docs/accept_loop.md | 6 +- .../async-chat/docs/all_together.md | 4 +- .../async-chat/docs/clean_shutdown.md | 116 +++++++++++++++++- .../docs/connecting_readers_and_writers.md | 17 +-- .../async-chat/docs/final_server_code.md | 4 +- .../async-chat/docs/handling_disconnection.md | 50 +++++--- .../async-chat/docs/implementing_a_client.md | 4 +- .../async-chat/docs/receiving_messages.md | 9 +- .../async-chat/docs/sending_messages.md | 5 +- 10 files changed, 181 insertions(+), 37 deletions(-) diff --git a/exercise-solutions/async-chat/book.toml b/exercise-solutions/async-chat/book.toml index 455a37f7..8281623f 100644 --- a/exercise-solutions/async-chat/book.toml +++ b/exercise-solutions/async-chat/book.toml @@ -4,3 +4,6 @@ language = "en" multilingual = false src = "docs" title = "Async programming tutorial in Rust with tokio" + +[rust] +edition = "2021" diff --git a/exercise-solutions/async-chat/docs/accept_loop.md b/exercise-solutions/async-chat/docs/accept_loop.md index 9a02c400..8c9bd398 100644 --- a/exercise-solutions/async-chat/docs/accept_loop.md +++ b/exercise-solutions/async-chat/docs/accept_loop.md @@ -6,7 +6,7 @@ First of all, let's add required import boilerplate: ```rust # extern crate tokio; -use std::future::Future, // 1 +use std::future::Future; // 1 use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, // 1 net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, // 3 @@ -61,7 +61,9 @@ Finally, let's add main: # extern crate tokio; # use tokio::net::{ToSocketAddrs}; # type Result = std::result::Result>; -# async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> {} +# async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { +# Ok(()) +# } # #[tokio::main] pub(crate) async fn main() -> Result<()> { diff --git a/exercise-solutions/async-chat/docs/all_together.md b/exercise-solutions/async-chat/docs/all_together.md index a69018fc..5e8b88c7 100644 --- a/exercise-solutions/async-chat/docs/all_together.md +++ b/exercise-solutions/async-chat/docs/all_together.md @@ -20,8 +20,8 @@ type Result = std::result::Result type Sender = mpsc::UnboundedSender; type Receiver = mpsc::UnboundedReceiver; -#[tokio::main] -async fn main() -> Result<()> { +// main +async fn run() -> Result<()> { accept_loop("127.0.0.1:8080").await } diff --git a/exercise-solutions/async-chat/docs/clean_shutdown.md b/exercise-solutions/async-chat/docs/clean_shutdown.md index 2fae9412..690b917e 100644 --- a/exercise-solutions/async-chat/docs/clean_shutdown.md +++ b/exercise-solutions/async-chat/docs/clean_shutdown.md @@ -21,6 +21,43 @@ However, we never wait for broker and writers, which might cause some messages t Let's add waiting to the server: ```rust +# extern crate tokio; +# use std::{ +# collections::hash_map::{Entry, HashMap}, +# future::Future, +# }; +# use tokio::{ +# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, +# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, +# sync::{mpsc, oneshot}, +# task, +# }; +# type Result = std::result::Result>; +# type Sender = mpsc::UnboundedSender; +# type Receiver = mpsc::UnboundedReceiver; +# enum Event { +# NewPeer { +# name: String, +# stream: OwnedWriteHalf, +# shutdown: oneshot::Receiver<()>, +# }, +# Message { +# from: String, +# to: Vec, +# msg: String, +# }, +# } +# async fn broker_loop(mut events: Receiver) {} +# async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { +# Ok(()) +# } +# fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +# where +# F: Future> + Send + 'static, +# { +# unimplemented!() +# } +# async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let listener = TcpListener::bind(addr).await?; @@ -40,9 +77,24 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { Event + connection_loop: ```rust +# extern crate tokio; +# use std::{ +# collections::hash_map::{Entry, HashMap}, +# future::Future, +# }; +# use tokio::{ +# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, +# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, +# sync::{mpsc, oneshot}, +# task, +# }; +# type Result = std::result::Result>; +# type Sender = mpsc::UnboundedSender; +# type Receiver = mpsc::UnboundedReceiver; +# #[derive(Debug)] enum Event { - NewPeer {unchanged + NewPeer { name: String, stream: OwnedWriteHalf, shutdown: oneshot::Receiver<()>, @@ -51,6 +103,10 @@ enum Event { } async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { + # let (reader, writer) = stream.into_split(); + # let reader = BufReader::new(reader); + # let mut lines = reader.lines(); + # let name = String::new(); // ... let (_shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); broker @@ -72,6 +128,49 @@ async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> And to the broker: ```rust +# extern crate tokio; +# use std::{ +# collections::hash_map::{Entry, HashMap}, +# future::Future, +# }; +# use tokio::{ +# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, +# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, +# sync::{mpsc, oneshot}, +# task, +# }; +# type Result = std::result::Result>; +# type Sender = mpsc::UnboundedSender; +# type Receiver = mpsc::UnboundedReceiver; +# enum Event { +# NewPeer { +# name: String, +# stream: OwnedWriteHalf, +# shutdown: oneshot::Receiver<()>, +# }, +# Message { +# from: String, +# to: Vec, +# msg: String, +# }, +# } +# async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { +# Ok(()) +# } +# fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +# where +# F: Future> + Send + 'static, +# { +# unimplemented!() +# } +# async fn connection_writer_loop( +# messages: &mut Receiver, +# stream: &mut OwnedWriteHalf, +# mut shutdown: oneshot::Receiver<()>, +# ) -> Result<()> { +# Ok(()) +# } +# async fn broker_loop(mut events: Receiver) { let mut peers: HashMap> = HashMap::new(); @@ -102,6 +201,21 @@ async fn broker_loop(mut events: Receiver) { connection_writer_loop: ```rust +# extern crate tokio; +# use std::{ +# collections::hash_map::{Entry, HashMap}, +# future::Future, +# }; +# use tokio::{ +# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, +# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, +# sync::{mpsc, oneshot}, +# task, +# }; +# type Result = std::result::Result>; +# type Sender = mpsc::UnboundedSender; +# type Receiver = mpsc::UnboundedReceiver; +# async fn connection_writer_loop( messages: &mut Receiver, stream: &mut OwnedWriteHalf, diff --git a/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md b/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md index db7746b2..5e3ae430 100644 --- a/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md +++ b/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md @@ -12,10 +12,7 @@ The order of events "Bob sends message to Alice" and "Alice joins" is determined ```rust # extern crate tokio; -# use std::{ -# collections::hash_map::{Entry, HashMap}, -# future::Future, -# }; +# use std::future::Future; # use tokio::{ # io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, # net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, @@ -27,14 +24,18 @@ The order of events "Bob sends message to Alice" and "Alice joins" is determined # type Receiver = mpsc::UnboundedReceiver; # # async fn connection_writer_loop( -# mut messages: Receiver, -# stream: Arc, -# ) -> Result<()> {} +# messages: &mut Receiver, +# stream: &mut OwnedWriteHalf, +# ) -> Result<()> { +# Ok(()) +# } # # fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> # where # F: Future> + Send + 'static, -# {} +# { +# unimplemented!() +# } # use std::collections::hash_map::{Entry, HashMap}; diff --git a/exercise-solutions/async-chat/docs/final_server_code.md b/exercise-solutions/async-chat/docs/final_server_code.md index 3076e38e..0544e408 100644 --- a/exercise-solutions/async-chat/docs/final_server_code.md +++ b/exercise-solutions/async-chat/docs/final_server_code.md @@ -20,8 +20,8 @@ type Result = std::result::Result type Sender = mpsc::UnboundedSender; type Receiver = mpsc::UnboundedReceiver; -#[tokio::main] -pub(crate) async fn main() -> Result<()> { +// main +async fn run() -> Result<()> { accept_loop("127.0.0.1:8080").await } diff --git a/exercise-solutions/async-chat/docs/handling_disconnection.md b/exercise-solutions/async-chat/docs/handling_disconnection.md index f8cab8d4..b1f6060a 100644 --- a/exercise-solutions/async-chat/docs/handling_disconnection.md +++ b/exercise-solutions/async-chat/docs/handling_disconnection.md @@ -19,16 +19,31 @@ First, let's add a shutdown channel to the `connection_loop`: ```rust # extern crate tokio; -# extern crate futures; -# use tokio::net::TcpStream; -# use futures::channel::mpsc; -# use futures::sink::SinkExt; -# use std::sync::Arc; -# +# use std::future::Future; +# use tokio::{ +# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, +# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, +# sync::{mpsc, oneshot}, +# task, +# }; # type Result = std::result::Result>; # type Sender = mpsc::UnboundedSender; # type Receiver = mpsc::UnboundedReceiver; # +# async fn connection_writer_loop( +# messages: &mut Receiver, +# stream: &mut OwnedWriteHalf, +# ) -> Result<()> { +# Ok(()) +# } +# +# fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +# where +# F: Future> + Send + 'static, +# { +# unimplemented!() +# } +# #[derive(Debug)] enum Event { @@ -44,9 +59,12 @@ enum Event { }, } -async fn connection_loop(mut broker: Sender, stream: Arc) -> Result<()> { +async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { + # let (reader, writer) = stream.into_split(); + # let reader = BufReader::new(reader); + # let mut lines = reader.lines(); + # let name: String = String::new(); // ... -# let name: String = unimplemented!(); let (_shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); broker .send(Event::NewPeer { @@ -69,15 +87,17 @@ We use the `select` macro for this purpose: ```rust # extern crate tokio; -# extern crate futures; -# use tokio::{net::TcpStream, prelude::*}; -# use futures::channel::mpsc; -use futures::{select, FutureExt}; -# use std::sync::Arc; -# type Receiver = mpsc::UnboundedReceiver; +# use std::future::Future; +# use tokio::{ +# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, +# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, +# sync::{mpsc, oneshot}, +# task, +# }; # type Result = std::result::Result>; # type Sender = mpsc::UnboundedSender; - +# type Receiver = mpsc::UnboundedReceiver; +# async fn connection_writer_loop( messages: &mut Receiver, stream: &mut OwnedWriteHalf, diff --git a/exercise-solutions/async-chat/docs/implementing_a_client.md b/exercise-solutions/async-chat/docs/implementing_a_client.md index 82c3b1c3..6db58a4d 100644 --- a/exercise-solutions/async-chat/docs/implementing_a_client.md +++ b/exercise-solutions/async-chat/docs/implementing_a_client.md @@ -21,8 +21,8 @@ use tokio::{ type Result = std::result::Result>; -#[tokio::main] -async fn main() -> Result<()> { +// main +async fn run() -> Result<()> { try_main("127.0.0.1:8080").await } diff --git a/exercise-solutions/async-chat/docs/receiving_messages.md b/exercise-solutions/async-chat/docs/receiving_messages.md index 0806308c..78e591d5 100644 --- a/exercise-solutions/async-chat/docs/receiving_messages.md +++ b/exercise-solutions/async-chat/docs/receiving_messages.md @@ -85,12 +85,14 @@ We can "fix" it by waiting for the task to be joined, like this: # task, # }; # type Result = std::result::Result>; -# async fn connection_loop(stream: TcpStream) -> Result<()> {} +# async fn connection_loop(stream: TcpStream) -> Result<()> { +# Ok(()) +# } # -# async move |stream| { +# async fn accept_loop(stream: TcpStream) -> Result<()> { let handle = task::spawn(connection_loop(stream)); handle.await? -# }; +# } ``` The `.await` waits until the client finishes, and `?` propagates the result. @@ -107,6 +109,7 @@ So let's use a helper function for this: # extern crate tokio; # use std::future::Future; # use tokio::task; +# type Result = std::result::Result>; fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> where F: Future> + Send + 'static, diff --git a/exercise-solutions/async-chat/docs/sending_messages.md b/exercise-solutions/async-chat/docs/sending_messages.md index 36faa90a..8cbe4f4e 100644 --- a/exercise-solutions/async-chat/docs/sending_messages.md +++ b/exercise-solutions/async-chat/docs/sending_messages.md @@ -21,7 +21,7 @@ If Alice and Charley send two messages to Bob at the same time, Bob will see the # use tokio::{ # io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, # net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, -# sync::{mpsc, oneshot}, +# sync::oneshot, # task, # }; # @@ -49,8 +49,9 @@ async fn connection_writer_loop( ```rust # extern crate tokio; # use tokio::net::TcpStream; - # let stream: TcpStream = unimplemented!(); + # async fn connection_loop(stream: TcpStream) { # use tokio::net::tcp; let (reader, writer): (tcp::OwnedReadHalf, tcp::OwnedWriteHalf) = stream.into_split(); + # } ``` From 8b9bb67f18146049c9db0b723ad8f8492ed3c925 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 17:08:51 +0200 Subject: [PATCH 26/36] Disable running code snippets (they should just compile, but don't execute) --- exercise-solutions/async-chat/book.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/exercise-solutions/async-chat/book.toml b/exercise-solutions/async-chat/book.toml index 8281623f..75742f56 100644 --- a/exercise-solutions/async-chat/book.toml +++ b/exercise-solutions/async-chat/book.toml @@ -7,3 +7,6 @@ title = "Async programming tutorial in Rust with tokio" [rust] edition = "2021" + +[output.html.playground] +runnable = false From 371b87e595d8b4c6a654a114d853e5796eac702b Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 17:20:51 +0200 Subject: [PATCH 27/36] Add disconnect message --- exercise-solutions/async-chat/src/server.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index be50c4e5..5e9eaeac 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -124,6 +124,7 @@ async fn broker_loop(mut events: Receiver) { disconnect = disconnect_receiver.recv() => { let (name, _pending_messages) = disconnect.unwrap(); assert!(peers.remove(&name).is_some()); + println!("user {} disconnected", name); continue; }, }; From fc3f53c68c72607eecd265c706ce83dcd2edc658 Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 17:29:51 +0200 Subject: [PATCH 28/36] Move async chat into book --- exercise-book/src/SUMMARY.md | 14 ++++++++++++++ .../src/async-chat}/accept_loop.md | 0 .../src/async-chat}/all_together.md | 0 .../src/async-chat}/clean_shutdown.md | 0 .../async-chat}/connecting_readers_and_writers.md | 0 .../src/async-chat}/final_server_code.md | 0 .../src/async-chat}/handling_disconnection.md | 0 .../src/async-chat}/implementing_a_client.md | 0 .../docs => exercise-book/src/async-chat}/index.md | 0 .../src/async-chat}/receiving_messages.md | 0 .../src/async-chat}/sending_messages.md | 0 .../src/async-chat}/specification.md | 0 exercise-solutions/async-chat/book.toml | 12 ------------ exercise-solutions/async-chat/docs/SUMMARY.md | 13 ------------- 14 files changed, 14 insertions(+), 25 deletions(-) rename {exercise-solutions/async-chat/docs => exercise-book/src/async-chat}/accept_loop.md (100%) rename {exercise-solutions/async-chat/docs => exercise-book/src/async-chat}/all_together.md (100%) rename {exercise-solutions/async-chat/docs => exercise-book/src/async-chat}/clean_shutdown.md (100%) rename {exercise-solutions/async-chat/docs => exercise-book/src/async-chat}/connecting_readers_and_writers.md (100%) rename {exercise-solutions/async-chat/docs => exercise-book/src/async-chat}/final_server_code.md (100%) rename {exercise-solutions/async-chat/docs => exercise-book/src/async-chat}/handling_disconnection.md (100%) rename {exercise-solutions/async-chat/docs => exercise-book/src/async-chat}/implementing_a_client.md (100%) rename {exercise-solutions/async-chat/docs => exercise-book/src/async-chat}/index.md (100%) rename {exercise-solutions/async-chat/docs => exercise-book/src/async-chat}/receiving_messages.md (100%) rename {exercise-solutions/async-chat/docs => exercise-book/src/async-chat}/sending_messages.md (100%) rename {exercise-solutions/async-chat/docs => exercise-book/src/async-chat}/specification.md (100%) delete mode 100644 exercise-solutions/async-chat/book.toml delete mode 100644 exercise-solutions/async-chat/docs/SUMMARY.md diff --git a/exercise-book/src/SUMMARY.md b/exercise-book/src/SUMMARY.md index 130dd654..61c6e65d 100644 --- a/exercise-book/src/SUMMARY.md +++ b/exercise-book/src/SUMMARY.md @@ -113,4 +113,18 @@ - [Interactive TCP Echo Server](./tcp-server.md) - [Share data between connections](./tcp-server-log.md) +# Async chat + +- [Tutorial: Implementing a chat](./async-chat/index.md) + - [Specification and Getting started](./async-chat/specification.md) + - [Writing an Accept Loop](./async-chat/accept_loop.md) + - [Receiving Messages](./async-chat/receiving_messages.md) + - [Sending Messages](./async-chat/sending_messages.md) + - [Connecting Readers and Writers](./async-chat/connecting_readers_and_writers.md) + - [All Together](./async-chat/all_together.md) + - [Clean Shutdown](./async-chat/clean_shutdown.md) + - [Handling Disconnection](./async-chat/handling_disconnection.md) + - [Final Server Code](./async-chat/final_server_code.md) + - [Implementing a Client](./async-chat/implementing_a_client.md) + # Other Topics diff --git a/exercise-solutions/async-chat/docs/accept_loop.md b/exercise-book/src/async-chat/accept_loop.md similarity index 100% rename from exercise-solutions/async-chat/docs/accept_loop.md rename to exercise-book/src/async-chat/accept_loop.md diff --git a/exercise-solutions/async-chat/docs/all_together.md b/exercise-book/src/async-chat/all_together.md similarity index 100% rename from exercise-solutions/async-chat/docs/all_together.md rename to exercise-book/src/async-chat/all_together.md diff --git a/exercise-solutions/async-chat/docs/clean_shutdown.md b/exercise-book/src/async-chat/clean_shutdown.md similarity index 100% rename from exercise-solutions/async-chat/docs/clean_shutdown.md rename to exercise-book/src/async-chat/clean_shutdown.md diff --git a/exercise-solutions/async-chat/docs/connecting_readers_and_writers.md b/exercise-book/src/async-chat/connecting_readers_and_writers.md similarity index 100% rename from exercise-solutions/async-chat/docs/connecting_readers_and_writers.md rename to exercise-book/src/async-chat/connecting_readers_and_writers.md diff --git a/exercise-solutions/async-chat/docs/final_server_code.md b/exercise-book/src/async-chat/final_server_code.md similarity index 100% rename from exercise-solutions/async-chat/docs/final_server_code.md rename to exercise-book/src/async-chat/final_server_code.md diff --git a/exercise-solutions/async-chat/docs/handling_disconnection.md b/exercise-book/src/async-chat/handling_disconnection.md similarity index 100% rename from exercise-solutions/async-chat/docs/handling_disconnection.md rename to exercise-book/src/async-chat/handling_disconnection.md diff --git a/exercise-solutions/async-chat/docs/implementing_a_client.md b/exercise-book/src/async-chat/implementing_a_client.md similarity index 100% rename from exercise-solutions/async-chat/docs/implementing_a_client.md rename to exercise-book/src/async-chat/implementing_a_client.md diff --git a/exercise-solutions/async-chat/docs/index.md b/exercise-book/src/async-chat/index.md similarity index 100% rename from exercise-solutions/async-chat/docs/index.md rename to exercise-book/src/async-chat/index.md diff --git a/exercise-solutions/async-chat/docs/receiving_messages.md b/exercise-book/src/async-chat/receiving_messages.md similarity index 100% rename from exercise-solutions/async-chat/docs/receiving_messages.md rename to exercise-book/src/async-chat/receiving_messages.md diff --git a/exercise-solutions/async-chat/docs/sending_messages.md b/exercise-book/src/async-chat/sending_messages.md similarity index 100% rename from exercise-solutions/async-chat/docs/sending_messages.md rename to exercise-book/src/async-chat/sending_messages.md diff --git a/exercise-solutions/async-chat/docs/specification.md b/exercise-book/src/async-chat/specification.md similarity index 100% rename from exercise-solutions/async-chat/docs/specification.md rename to exercise-book/src/async-chat/specification.md diff --git a/exercise-solutions/async-chat/book.toml b/exercise-solutions/async-chat/book.toml deleted file mode 100644 index 75742f56..00000000 --- a/exercise-solutions/async-chat/book.toml +++ /dev/null @@ -1,12 +0,0 @@ -[book] -authors = ["Ferrous Systems GmbH"] -language = "en" -multilingual = false -src = "docs" -title = "Async programming tutorial in Rust with tokio" - -[rust] -edition = "2021" - -[output.html.playground] -runnable = false diff --git a/exercise-solutions/async-chat/docs/SUMMARY.md b/exercise-solutions/async-chat/docs/SUMMARY.md deleted file mode 100644 index 9c5a7112..00000000 --- a/exercise-solutions/async-chat/docs/SUMMARY.md +++ /dev/null @@ -1,13 +0,0 @@ -# Summary - -- [Tutorial: Implementing a chat](./index.md) - - [Specification and Getting started](./specification.md) - - [Writing an Accept Loop](./accept_loop.md) - - [Receiving Messages](./receiving_messages.md) - - [Sending Messages](./sending_messages.md) - - [Connecting Readers and Writers](./connecting_readers_and_writers.md) - - [All Together](./all_together.md) - - [Clean Shutdown](./clean_shutdown.md) - - [Handling Disconnection](./handling_disconnection.md) - - [Final Server Code](./final_server_code.md) - - [Implementing a Client](./implementing_a_client.md) From 875c1d2272a73bf8a352c629e80619164be5893d Mon Sep 17 00:00:00 2001 From: Johann Hemmann Date: Mon, 27 May 2024 17:35:21 +0200 Subject: [PATCH 29/36] tokio --- exercise-book/src/async-chat/clean_shutdown.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercise-book/src/async-chat/clean_shutdown.md b/exercise-book/src/async-chat/clean_shutdown.md index 690b917e..0d5a866c 100644 --- a/exercise-book/src/async-chat/clean_shutdown.md +++ b/exercise-book/src/async-chat/clean_shutdown.md @@ -11,7 +11,7 @@ A more correct shutdown sequence would be: A clean shutdown in a channel based architecture is easy, although it can appear a magic trick at first. In Rust, receiver side of a channel is closed as soon as all senders are dropped. That is, as soon as producers exit and drop their senders, the rest of the system shuts down naturally. -In `async_std` this translates to two rules: +In `tokio` this translates to two rules: 1. Make sure that channels form an acyclic graph. 2. Take care to wait, in the correct order, until intermediate layers of the system process pending messages. From de6b0defc2f27134d0ef8d4ba6e7d9709f87abbe Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Mon, 27 May 2024 22:44:29 +0000 Subject: [PATCH 30/36] Implement actually working cancellation --- exercise-book/src/async-chat/accept_loop.md | 4 +- exercise-book/src/async-chat/index.md | 2 +- .../src/async-chat/sending_messages.md | 7 +- exercise-solutions/async-chat/src/client.rs | 2 + exercise-solutions/async-chat/src/server.rs | 78 +++++++----- .../async-chat/step2/Cargo.toml | 7 ++ .../async-chat/step2/src/client.rs | 41 +++++++ .../async-chat/step2/src/main.rs | 13 ++ .../async-chat/step2/src/server.rs | 112 ++++++++++++++++++ 9 files changed, 227 insertions(+), 39 deletions(-) create mode 100644 exercise-templates/async-chat/step2/Cargo.toml create mode 100644 exercise-templates/async-chat/step2/src/client.rs create mode 100644 exercise-templates/async-chat/step2/src/main.rs create mode 100644 exercise-templates/async-chat/step2/src/server.rs diff --git a/exercise-book/src/async-chat/accept_loop.md b/exercise-book/src/async-chat/accept_loop.md index 8c9bd398..53513551 100644 --- a/exercise-book/src/async-chat/accept_loop.md +++ b/exercise-book/src/async-chat/accept_loop.md @@ -20,8 +20,8 @@ type Result = std::result::Result 1. Import some traits required to work with futures and streams. 2. The `task` module roughly corresponds to the `std::thread` module, but tasks are much lighter weight. A single thread can run many tasks. -3. For the socket type, we use `TcpListener` from `tokio`, which is just like `std::net::TcpListener`, but is non-blocking and uses `async` API. -4. We will skip implementing comprehensive error handling in this example. +3. For the socket type, we use `TcpListener` from `tokio`, which is similar to the sync `std::net::TcpListener`, but is non-blocking and uses `async` API. +4. We will skip implementing detailled error handling in this example. To propagate the errors, we will use a boxed error trait object. Do you know that there's `From<&'_ str> for Box` implementation in stdlib, which allows you to use strings with `?` operator? diff --git a/exercise-book/src/async-chat/index.md b/exercise-book/src/async-chat/index.md index f0ccf155..bb24049c 100644 --- a/exercise-book/src/async-chat/index.md +++ b/exercise-book/src/async-chat/index.md @@ -1,4 +1,4 @@ -# Tutorial: Writing a chat +# Writing an async chat Nothing is simpler than creating a chat server, right? Not quite, chat servers expose you to all the fun of asynchronous programming: diff --git a/exercise-book/src/async-chat/sending_messages.md b/exercise-book/src/async-chat/sending_messages.md index 8cbe4f4e..e6088ae1 100644 --- a/exercise-book/src/async-chat/sending_messages.md +++ b/exercise-book/src/async-chat/sending_messages.md @@ -1,14 +1,9 @@ ## Sending Messages Now it's time to implement the other half -- sending messages. -A most obvious way to implement sending is to give each `connection_loop` access to the write half of `TcpStream` of each other clients. -That way, a client can directly `.write_all` a message to recipients. -However, this would be wrong: if Alice sends `bob: foo`, and Charley sends `bob: bar`, Bob might actually receive `fobaor`. -Sending a message over a socket might require several syscalls, so two concurrent `.write_all`'s might interfere with each other! - As a rule of thumb, only a single task should write to each `TcpStream`. +This way, we also have compartmentalised that activity and automatically serialize all outgoing messages. So let's create a `connection_writer_loop` task which receives messages over a channel and writes them to the socket. -This task would be the point of serialization of messages. If Alice and Charley send two messages to Bob at the same time, Bob will see the messages in the same order as they arrive in the channel. ```rust diff --git a/exercise-solutions/async-chat/src/client.rs b/exercise-solutions/async-chat/src/client.rs index b1bb8ead..a4894384 100644 --- a/exercise-solutions/async-chat/src/client.rs +++ b/exercise-solutions/async-chat/src/client.rs @@ -34,5 +34,7 @@ async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { } } } + + println!("Client disconnected! Hit enter to quit."); Ok(()) } diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index 5e9eaeac..1b70616b 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -1,13 +1,15 @@ use std::{ collections::hash_map::{Entry, HashMap}, future::Future, + sync::Arc, }; use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, - sync::{mpsc, oneshot}, + sync::{mpsc, oneshot, Notify}, task, + time::{sleep, Duration}, }; type Result = std::result::Result>; @@ -24,17 +26,25 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); let broker = task::spawn(broker_loop(broker_receiver)); + let shutdown_notification = Arc::new(Notify::new()); - while let Ok((stream, _socket_addr)) = listener.accept().await { - println!("Accepting from: {}", stream.peer_addr()?); - spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); + loop { + tokio::select!{ + Ok((stream, _socket_addr)) = listener.accept() => { + println!("Accepting from: {}", stream.peer_addr()?); + spawn_and_log_error(connection_loop(broker_sender.clone(), stream, shutdown_notification.clone())); + }, + _ = tokio::signal::ctrl_c() => break, + } } + println!("Shutting down!"); + shutdown_notification.notify_waiters(); drop(broker_sender); broker.await?; Ok(()) } -async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { +async fn connection_loop(broker: Sender, stream: TcpStream, shutdown: Arc) -> Result<()> { let (reader, writer) = stream.into_split(); let reader = BufReader::new(reader); let mut lines = reader.lines(); @@ -47,42 +57,47 @@ async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> println!("user {} connected", name); - let (_shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); broker .send(Event::NewPeer { name: name.clone(), stream: writer, - shutdown: shutdown_receiver, + shutdown: shutdown.clone(), }) .unwrap(); - - while let Ok(Some(line)) = lines.next_line().await { - let (dest, msg) = match line.split_once(':') { - None => continue, - Some((dest, msg)) => (dest, msg.trim()), - }; - let dest: Vec = dest - .split(',') - .map(|name| name.trim().to_string()) - .collect(); - let msg: String = msg.trim().to_string(); - - broker - .send(Event::Message { - from: name.clone(), - to: dest, - msg, - }) - .unwrap(); + + loop { + tokio::select! { + Ok(Some(line)) = lines.next_line() => { + let (dest, msg) = match line.split_once(':') { + + None => continue, + Some((dest, msg)) => (dest, msg.trim()), + }; + let dest: Vec = dest + .split(',') + .map(|name| name.trim().to_string()) + .collect(); + let msg: String = msg.trim().to_string(); + + broker + .send(Event::Message { + from: name.clone(), + to: dest, + msg, + }) + .unwrap(); + }, + _ = shutdown.notified() => break, + } } - + println!("Closing connection loop!"); Ok(()) } async fn connection_writer_loop( messages: &mut Receiver, stream: &mut OwnedWriteHalf, - mut shutdown: oneshot::Receiver<()>, + mut shutdown: Arc, ) -> Result<()> { loop { tokio::select! { @@ -90,9 +105,12 @@ async fn connection_writer_loop( Some(msg) => stream.write_all(msg.as_bytes()).await?, None => break, }, - _ = &mut shutdown => break + _ = shutdown.notified() => break } } + + println!("Closing connection_writer loop!"); + Ok(()) } @@ -101,7 +119,7 @@ enum Event { NewPeer { name: String, stream: OwnedWriteHalf, - shutdown: oneshot::Receiver<()>, + shutdown: Arc, }, Message { from: String, diff --git a/exercise-templates/async-chat/step2/Cargo.toml b/exercise-templates/async-chat/step2/Cargo.toml new file mode 100644 index 00000000..90505301 --- /dev/null +++ b/exercise-templates/async-chat/step2/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "async-chat" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["full"] } diff --git a/exercise-templates/async-chat/step2/src/client.rs b/exercise-templates/async-chat/step2/src/client.rs new file mode 100644 index 00000000..3fb5b098 --- /dev/null +++ b/exercise-templates/async-chat/step2/src/client.rs @@ -0,0 +1,41 @@ +use tokio::{ + io::{stdin, AsyncBufReadExt, AsyncWriteExt, BufReader}, + net::{TcpStream, ToSocketAddrs}, +}; + +type Result = std::result::Result>; + +#[tokio::main] +pub(crate) async fn main() -> Result<()> { + try_main("127.0.0.1:8080").await +} + +async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { + let stream = TcpStream::connect(addr).await?; + let (reader, mut writer) = tokio::io::split(stream); + let reader = BufReader::new(reader); + let mut lines_from_server = reader.lines(); + + let stdin = BufReader::new(stdin()); + let mut lines_from_stdin = stdin.lines(); + loop { + tokio::select! { + line = lines_from_server.next_line() => match line { + Ok(Some(line)) => { + println!("{}", line); + }, + Ok(None) => break, + Err(e) => eprintln!("Error {:?}:", e), + }, + line = lines_from_stdin.next_line() => match line { + Ok(Some(line)) => { + writer.write_all(line.as_bytes()).await?; + writer.write_all(b"\n").await?; + }, + Ok(None) => break, + Err(e) => eprintln!("Error {:?}:", e), + } + } + } + Ok(()) +} diff --git a/exercise-templates/async-chat/step2/src/main.rs b/exercise-templates/async-chat/step2/src/main.rs new file mode 100644 index 00000000..89e5e2b6 --- /dev/null +++ b/exercise-templates/async-chat/step2/src/main.rs @@ -0,0 +1,13 @@ +mod client; +mod server; + +type Result = std::result::Result>; + +fn main() -> Result<()> { + let mut args = std::env::args(); + match (args.nth(1).as_ref().map(String::as_str), args.next()) { + (Some("client"), None) => client::main(), + (Some("server"), None) => server::main(), + _ => Err("Usage: a-chat [client|server]".into()), + } +} diff --git a/exercise-templates/async-chat/step2/src/server.rs b/exercise-templates/async-chat/step2/src/server.rs new file mode 100644 index 00000000..0484e3ec --- /dev/null +++ b/exercise-templates/async-chat/step2/src/server.rs @@ -0,0 +1,112 @@ +use std::{ + collections::hash_map::{Entry, HashMap}, + future::Future, +}; + +use tokio::sync::{mpsc, oneshot}; + +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + net::tcp::OwnedWriteHalf, + net::{TcpListener, TcpStream, ToSocketAddrs}, + task, +}; + +type Result = std::result::Result>; +type Sender = mpsc::UnboundedSender; +type Receiver = mpsc::UnboundedReceiver; + +#[tokio::main] +pub(crate) async fn main() -> Result<()> { + accept_loop("127.0.0.1:8080").await +} + +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + let listener = TcpListener::bind(addr).await?; + + let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); + let broker = task::spawn(broker_loop(broker_receiver)); + + while let Ok((stream, _socket_addr)) = listener.accept().await { + println!("Accepting from: {}", stream.peer_addr()?); + spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); + } + drop(broker_sender); + broker.await?; + Ok(()) +} + +async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { + let (reader, writer) = stream.into_split(); + let reader = BufReader::new(reader); + let mut lines = reader.lines(); + + let name = match lines.next_line().await { + Ok(Some(line)) => line, + Ok(None) => return Err("peer disconnected immediately".into()), + Err(e) => return Err(Box::new(e)), + }; + + println!("user {} connected", name); + + while let Ok(Some(line)) = lines.next_line().await { + let (dest, msg) = match line.find(':') { + None => continue, + Some(idx) => (&line[..idx], line[idx + 1..].trim()), + }; + let dest: Vec = dest + .split(',') + .map(|name| name.trim().to_string()) + .collect(); + let msg: String = msg.trim().to_string(); + + println!("User {} sent message: {}", name, msg) + } + + Ok(()) +} + +async fn connection_writer_loop( + messages: &mut Receiver, + stream: &mut OwnedWriteHalf, + mut shutdown: oneshot::Receiver<()>, +) -> Result<()> { + loop { + let msg = messages.recv().await; + match msg { + Some(msg) => stream.write_all(msg.as_bytes()).await?, + None => break, + } + } + Ok(()) +} + +#[derive(Debug)] +enum Event { + NewPeer { + name: String, + stream: OwnedWriteHalf, + shutdown: oneshot::Receiver<()>, + }, + Message { + from: String, + to: Vec, + msg: String, + }, +} + +async fn broker_loop(mut events: Receiver) { + loop { + } +} + +fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> +where + F: Future> + Send + 'static, +{ + task::spawn(async move { + if let Err(e) = fut.await { + eprintln!("{}", e) + } + }) +} From 5707efed623a5784d82a8f43160196f83dfbd128 Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Tue, 28 May 2024 04:35:59 +0000 Subject: [PATCH 31/36] Sizable fixups --- exercise-book/src/SUMMARY.md | 2 +- exercise-book/src/async-chat/accept_loop.md | 13 +-- .../src/async-chat/clean_shutdown.md | 106 +++--------------- .../connecting_readers_and_writers.md | 7 +- .../src/async-chat/handling_disconnection.md | 54 ++++++++- .../src/async-chat/receiving_messages.md | 28 +++-- .../src/async-chat/sending_messages.md | 10 +- exercise-solutions/async-chat/src/server.rs | 13 ++- .../async-chat/step1/src/server.rs | 89 +-------------- .../async-chat/step2/Cargo.toml | 2 +- .../async-chat/step2/src/server.rs | 42 +------ 11 files changed, 118 insertions(+), 248 deletions(-) diff --git a/exercise-book/src/SUMMARY.md b/exercise-book/src/SUMMARY.md index 61c6e65d..1a65afdc 100644 --- a/exercise-book/src/SUMMARY.md +++ b/exercise-book/src/SUMMARY.md @@ -115,7 +115,7 @@ # Async chat -- [Tutorial: Implementing a chat](./async-chat/index.md) +- [Implementing a chat](./async-chat/index.md) - [Specification and Getting started](./async-chat/specification.md) - [Writing an Accept Loop](./async-chat/accept_loop.md) - [Receiving Messages](./async-chat/receiving_messages.md) diff --git a/exercise-book/src/async-chat/accept_loop.md b/exercise-book/src/async-chat/accept_loop.md index 53513551..3cda7556 100644 --- a/exercise-book/src/async-chat/accept_loop.md +++ b/exercise-book/src/async-chat/accept_loop.md @@ -35,9 +35,11 @@ Now we can write the server's accept loop: async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { // 1 let listener = TcpListener::bind(addr).await?; // 2 - while let Ok((stream, _socket_addr)) = listener.accept().await { // 3 + loop { // 3 + let (stream, _) = listener.accept().await?; // TODO } + Ok(()) } ``` @@ -46,14 +48,7 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { // 1 2. `TcpListener::bind` call returns a future, which we `.await` to extract the `Result`, and then `?` to get a `TcpListener`. Note how `.await` and `?` work nicely together. This is exactly how `std::net::TcpListener` works, but with `.await` added. -3. Here, we would like to iterate incoming sockets, similar to how one would do in `std`: - -```rust,should_panic -let listener: std::net::TcpListener = unimplemented!(); -for stream in listener.incoming() { - // ... -} -``` +3. We generally use `loop` and `break` for looping in Futures, that makes things easier down the line. Finally, let's add main: diff --git a/exercise-book/src/async-chat/clean_shutdown.md b/exercise-book/src/async-chat/clean_shutdown.md index 0d5a866c..085abdfb 100644 --- a/exercise-book/src/async-chat/clean_shutdown.md +++ b/exercise-book/src/async-chat/clean_shutdown.md @@ -18,6 +18,9 @@ In `tokio` this translates to two rules: In `a-chat`, we already have an unidirectional flow of messages: `reader -> broker -> writer`. However, we never wait for broker and writers, which might cause some messages to get dropped. + +We also need to notify all readers that we are going to stop accepting messages. Here, we use `tokio::sync::Notify`. + Let's add waiting to the server: ```rust @@ -29,7 +32,7 @@ Let's add waiting to the server: # use tokio::{ # io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, # net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, -# sync::{mpsc, oneshot}, +# sync::{mpsc, oneshot, Notify}, # task, # }; # type Result = std::result::Result>; @@ -63,68 +66,22 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); let broker = task::spawn(broker_loop(broker_receiver)); + let shutdown_notification = Arc::new(Notify::new()); + - while let Ok((stream, _socket_addr)) = listener.accept().await { + loop { + let (stream, _) = listener.accept().await?; println!("Accepting from: {}", stream.peer_addr()?); - spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); + spawn_and_log_error(connection_loop(broker_sender.clone(), stream, shutdown_notification)); } + println!("Shutting down server!"); + shutdown_notification.notify_waiters(); drop(broker_sender); // 1 broker.await?; // 5 Ok(()) } ``` -Event + connection_loop: - -```rust -# extern crate tokio; -# use std::{ -# collections::hash_map::{Entry, HashMap}, -# future::Future, -# }; -# use tokio::{ -# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, -# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, -# sync::{mpsc, oneshot}, -# task, -# }; -# type Result = std::result::Result>; -# type Sender = mpsc::UnboundedSender; -# type Receiver = mpsc::UnboundedReceiver; -# -#[derive(Debug)] -enum Event { - NewPeer { - name: String, - stream: OwnedWriteHalf, - shutdown: oneshot::Receiver<()>, - }, - Message { /* unchanged */ }, -} - -async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { - # let (reader, writer) = stream.into_split(); - # let reader = BufReader::new(reader); - # let mut lines = reader.lines(); - # let name = String::new(); - // ... - let (_shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); - broker - .send(Event::NewPeer { - name: name.clone(), - stream: writer, - shutdown: shutdown_receiver, - }) - .unwrap(); - - while let Ok(Some(line)) = lines.next_line().await { - // ... - } - - Ok(()) -} -``` - And to the broker: ```rust @@ -182,55 +139,20 @@ async fn broker_loop(mut events: Receiver) { Event::NewPeer { name, mut stream, - shutdown, } => match peers.entry(name.clone()) { Entry::Occupied(..) => (), Entry::Vacant(entry) => { let (client_sender, mut client_receiver) = mpsc::unbounded_channel(); entry.insert(client_sender); spawn_and_log_error(async move { - connection_writer_loop(&mut client_receiver, &mut stream, shutdown).await + connection_writer_loop(&mut client_receiver, &mut stream).await }); } }, } } -} -``` - -connection_writer_loop: -```rust -# extern crate tokio; -# use std::{ -# collections::hash_map::{Entry, HashMap}, -# future::Future, -# }; -# use tokio::{ -# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, -# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, -# sync::{mpsc, oneshot}, -# task, -# }; -# type Result = std::result::Result>; -# type Sender = mpsc::UnboundedSender; -# type Receiver = mpsc::UnboundedReceiver; -# -async fn connection_writer_loop( - messages: &mut Receiver, - stream: &mut OwnedWriteHalf, - mut shutdown: oneshot::Receiver<()>, -) -> Result<()> { - loop { - tokio::select! { - msg = messages.recv() => match msg { - Some(msg) => stream.write_all(msg.as_bytes()).await?, - None => break, - }, - _ = &mut shutdown => break - } - } - Ok(()) + drop(peers) //4 } ``` @@ -241,5 +163,5 @@ Notice what happens with all of the channels once we exit the accept loop: 2. Next, the broker exits `while let Some(event) = events.next().await` loop. 3. It's crucial that, at this stage, we drop the `peers` map. This drops writer's senders. -4. Now we can join all of the writers. +4. Tokio will automatically wait for all finishing futures 5. Finally, we join the broker, which also guarantees that all the writes have terminated. diff --git a/exercise-book/src/async-chat/connecting_readers_and_writers.md b/exercise-book/src/async-chat/connecting_readers_and_writers.md index 5e3ae430..549a3ef6 100644 --- a/exercise-book/src/async-chat/connecting_readers_and_writers.md +++ b/exercise-book/src/async-chat/connecting_readers_and_writers.md @@ -55,7 +55,12 @@ enum Event { // 1 async fn broker_loop(mut events: Receiver) { let mut peers: HashMap> = HashMap::new(); // 2 - while let Some(event) = events.recv().await { + loop { + let event = match events.recv().await { + Some(event) => event, + None => break, + }; + match event { Event::Message { from, to, msg } => { // 3 for addr in to { diff --git a/exercise-book/src/async-chat/handling_disconnection.md b/exercise-book/src/async-chat/handling_disconnection.md index b1f6060a..05b7b0a2 100644 --- a/exercise-book/src/async-chat/handling_disconnection.md +++ b/exercise-book/src/async-chat/handling_disconnection.md @@ -121,5 +121,55 @@ async fn connection_writer_loop( 3. In the shutdown case break the loop. Another problem is that between the moment we detect disconnection in `connection_writer_loop` and the moment when we actually remove the peer from the `peers` map, new messages might be pushed into the peer's channel. -To not lose these messages completely, we'll return the messages channel back to the broker. -This also allows us to establish a useful invariant that the message channel strictly outlives the peer in the `peers` map, and makes the broker itself infallible. + +The final thing to handle is actually clean up our peers map. Here, we need to establish a communication back to the broker. However, we can handle that completely within the brokers scope, to not infect the writer loop with this concern. + +To not lose these messages completely, we'll return the writers messages receiver back to the broker. This also allows us to establish a useful invariant that the message channel strictly outlives the peer in the peers map, and makes the broker itself infallible. + +```rust +async fn broker_loop(mut events: Receiver) { + let (disconnect_sender, mut disconnect_receiver) = + mpsc::unbounded_channel::<(String, Receiver)>(); // 1 + let mut peers: HashMap> = HashMap::new(); + + loop { + let event = tokio::select! { + event = events.recv() => match event { + None => break, + Some(event) => event, + }, + disconnect = disconnect_receiver.recv() => { + let (name, _pending_messages) = disconnect.unwrap(); + assert!(peers.remove(&name).is_some()); + println!("user {} disconnected", name); + continue; + }, + }; + match event { + Event::Message { from, to, msg } => { + // ... + } + Event::NewPeer { + name, + mut stream, + shutdown, + } => match peers.entry(name.clone()) { + Entry::Occupied(..) => (), + Entry::Vacant(entry) => { + // ... + spawn_and_log_error(async move { + let res = + connection_writer_loop(&mut client_receiver, &mut stream, shutdown) + .await; + println!("user {} disconnected", name); + disconnect_sender.send((name, client_receiver)).unwrap(); // 2 + res + }); + } + }, + } + } + drop(peers); + drop(disconnect_sender); + while let Some((_name, _pending_messages)) = disconnect_receiver.recv().await {} +} \ No newline at end of file diff --git a/exercise-book/src/async-chat/receiving_messages.md b/exercise-book/src/async-chat/receiving_messages.md index 78e591d5..75decd47 100644 --- a/exercise-book/src/async-chat/receiving_messages.md +++ b/exercise-book/src/async-chat/receiving_messages.md @@ -43,17 +43,23 @@ async fn connection_loop(stream: TcpStream) -> Result<()> { println!("name = {}", name); // 4 - while let Some(line) = lines.next_line().await? { - // 5 - let (dest, msg) = match line.find(':') { - None => continue, - Some(idx) => (&line[..idx], line[idx + 1..].trim()), - }; - let dest = dest - .split(',') - .map(|name| name.trim().to_string()) - .collect::>(); - let msg = msg.to_string(); + loop { + if let Some(line) = lines.next_line().await? { + // 5 + let (dest, msg) = match line.find(':') { + None => continue, + Some(idx) => (&line[..idx], line[idx + 1..].trim()), + }; + let dest = dest + .split(',') + .map(|name| name.trim().to_string()) + .collect::>(); + let msg = msg.to_string(); + // TODO: this is temporary + println!("Received message:", msg); + } else { + break + } } Ok(()) } diff --git a/exercise-book/src/async-chat/sending_messages.md b/exercise-book/src/async-chat/sending_messages.md index e6088ae1..cee89a6f 100644 --- a/exercise-book/src/async-chat/sending_messages.md +++ b/exercise-book/src/async-chat/sending_messages.md @@ -28,10 +28,14 @@ type Receiver = mpsc::UnboundedReceiver; async fn connection_writer_loop( messages: &mut Receiver, - stream: &mut OwnedWriteHalf, + stream: &mut OwnedWriteHalf ) -> Result<()> { - while let Some(msg) = messages.recv().await { - stream.write_all(msg.as_bytes()).await?; + loop { + let msg = messages.recv().await; + match msg { + Some(msg) => stream.write_all(msg.as_bytes()).await?, + None => break, + } } Ok(()) } diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index 1b70616b..b2491006 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -9,7 +9,6 @@ use tokio::{ net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, sync::{mpsc, oneshot, Notify}, task, - time::{sleep, Duration}, }; type Result = std::result::Result>; @@ -48,6 +47,7 @@ async fn connection_loop(broker: Sender, stream: TcpStream, shutdown: Arc let (reader, writer) = stream.into_split(); let reader = BufReader::new(reader); let mut lines = reader.lines(); + let (shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); let name = match lines.next_line().await { Ok(Some(line)) => line, @@ -61,7 +61,7 @@ async fn connection_loop(broker: Sender, stream: TcpStream, shutdown: Arc .send(Event::NewPeer { name: name.clone(), stream: writer, - shutdown: shutdown.clone(), + shutdown: shutdown_receiver, }) .unwrap(); @@ -91,13 +91,15 @@ async fn connection_loop(broker: Sender, stream: TcpStream, shutdown: Arc } } println!("Closing connection loop!"); + drop(shutdown_sender); + Ok(()) } async fn connection_writer_loop( messages: &mut Receiver, stream: &mut OwnedWriteHalf, - mut shutdown: Arc, + mut shutdown: oneshot::Receiver<()>, ) -> Result<()> { loop { tokio::select! { @@ -105,7 +107,7 @@ async fn connection_writer_loop( Some(msg) => stream.write_all(msg.as_bytes()).await?, None => break, }, - _ = shutdown.notified() => break + _ = &mut shutdown => break } } @@ -119,7 +121,7 @@ enum Event { NewPeer { name: String, stream: OwnedWriteHalf, - shutdown: Arc, + shutdown: oneshot::Receiver<()>, }, Message { from: String, @@ -169,6 +171,7 @@ async fn broker_loop(mut events: Receiver) { let res = connection_writer_loop(&mut client_receiver, &mut stream, shutdown) .await; + println!("user {} disconnected", name); disconnect_sender.send((name, client_receiver)).unwrap(); res }); diff --git a/exercise-templates/async-chat/step1/src/server.rs b/exercise-templates/async-chat/step1/src/server.rs index 0484e3ec..e6765e53 100644 --- a/exercise-templates/async-chat/step1/src/server.rs +++ b/exercise-templates/async-chat/step1/src/server.rs @@ -21,92 +21,13 @@ pub(crate) async fn main() -> Result<()> { accept_loop("127.0.0.1:8080").await } -async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { - let listener = TcpListener::bind(addr).await?; +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { // 1 + let listener = TcpListener::bind(addr).await?; // 2 - let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); - let broker = task::spawn(broker_loop(broker_receiver)); - - while let Ok((stream, _socket_addr)) = listener.accept().await { - println!("Accepting from: {}", stream.peer_addr()?); - spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); - } - drop(broker_sender); - broker.await?; - Ok(()) -} - -async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { - let (reader, writer) = stream.into_split(); - let reader = BufReader::new(reader); - let mut lines = reader.lines(); - - let name = match lines.next_line().await { - Ok(Some(line)) => line, - Ok(None) => return Err("peer disconnected immediately".into()), - Err(e) => return Err(Box::new(e)), - }; - - println!("user {} connected", name); - - while let Ok(Some(line)) = lines.next_line().await { - let (dest, msg) = match line.find(':') { - None => continue, - Some(idx) => (&line[..idx], line[idx + 1..].trim()), - }; - let dest: Vec = dest - .split(',') - .map(|name| name.trim().to_string()) - .collect(); - let msg: String = msg.trim().to_string(); - - println!("User {} sent message: {}", name, msg) + loop { // 3 + let (stream, _) = listener.accept().await?; + // TODO } Ok(()) } - -async fn connection_writer_loop( - messages: &mut Receiver, - stream: &mut OwnedWriteHalf, - mut shutdown: oneshot::Receiver<()>, -) -> Result<()> { - loop { - let msg = messages.recv().await; - match msg { - Some(msg) => stream.write_all(msg.as_bytes()).await?, - None => break, - } - } - Ok(()) -} - -#[derive(Debug)] -enum Event { - NewPeer { - name: String, - stream: OwnedWriteHalf, - shutdown: oneshot::Receiver<()>, - }, - Message { - from: String, - to: Vec, - msg: String, - }, -} - -async fn broker_loop(mut events: Receiver) { - loop { - } -} - -fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> -where - F: Future> + Send + 'static, -{ - task::spawn(async move { - if let Err(e) = fut.await { - eprintln!("{}", e) - } - }) -} diff --git a/exercise-templates/async-chat/step2/Cargo.toml b/exercise-templates/async-chat/step2/Cargo.toml index 90505301..81444d45 100644 --- a/exercise-templates/async-chat/step2/Cargo.toml +++ b/exercise-templates/async-chat/step2/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "async-chat" +name = "async-chat-2" version = "0.1.0" edition = "2021" diff --git a/exercise-templates/async-chat/step2/src/server.rs b/exercise-templates/async-chat/step2/src/server.rs index 0484e3ec..e962164d 100644 --- a/exercise-templates/async-chat/step2/src/server.rs +++ b/exercise-templates/async-chat/step2/src/server.rs @@ -24,19 +24,14 @@ pub(crate) async fn main() -> Result<()> { async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let listener = TcpListener::bind(addr).await?; - let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); - let broker = task::spawn(broker_loop(broker_receiver)); - while let Ok((stream, _socket_addr)) = listener.accept().await { println!("Accepting from: {}", stream.peer_addr()?); - spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); + task::spawn(connection_loop(stream)); } - drop(broker_sender); - broker.await?; Ok(()) } -async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { +async fn connection_loop(stream: TcpStream) -> Result<()> { let (reader, writer) = stream.into_split(); let reader = BufReader::new(reader); let mut lines = reader.lines(); @@ -68,8 +63,7 @@ async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> async fn connection_writer_loop( messages: &mut Receiver, - stream: &mut OwnedWriteHalf, - mut shutdown: oneshot::Receiver<()>, + stream: &mut OwnedWriteHalf ) -> Result<()> { loop { let msg = messages.recv().await; @@ -80,33 +74,3 @@ async fn connection_writer_loop( } Ok(()) } - -#[derive(Debug)] -enum Event { - NewPeer { - name: String, - stream: OwnedWriteHalf, - shutdown: oneshot::Receiver<()>, - }, - Message { - from: String, - to: Vec, - msg: String, - }, -} - -async fn broker_loop(mut events: Receiver) { - loop { - } -} - -fn spawn_and_log_error(fut: F) -> task::JoinHandle<()> -where - F: Future> + Send + 'static, -{ - task::spawn(async move { - if let Err(e) = fut.await { - eprintln!("{}", e) - } - }) -} From f639f5234f5de9bf1d5875ca8d8885a5a53c5d17 Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Tue, 28 May 2024 06:24:22 +0000 Subject: [PATCH 32/36] Final run through --- exercise-book/src/SUMMARY.md | 4 +- exercise-book/src/async-chat/all_together.md | 81 ++++++++------ .../src/async-chat/clean_shutdown.md | 70 +++++++++--- .../connecting_readers_and_writers.md | 7 +- .../src/async-chat/final_server_code.md | 102 ++++++++++-------- .../src/async-chat/handling_disconnection.md | 19 +--- .../src/async-chat/receiving_messages.md | 9 +- .../src/async-chat/sending_messages.md | 4 +- exercise-solutions/async-chat/src/client.rs | 2 +- .../async-chat/step1/src/client.rs | 11 +- .../async-chat/step2/src/client.rs | 11 +- 11 files changed, 193 insertions(+), 127 deletions(-) diff --git a/exercise-book/src/SUMMARY.md b/exercise-book/src/SUMMARY.md index 1a65afdc..a95cfec2 100644 --- a/exercise-book/src/SUMMARY.md +++ b/exercise-book/src/SUMMARY.md @@ -120,8 +120,8 @@ - [Writing an Accept Loop](./async-chat/accept_loop.md) - [Receiving Messages](./async-chat/receiving_messages.md) - [Sending Messages](./async-chat/sending_messages.md) - - [Connecting Readers and Writers](./async-chat/connecting_readers_and_writers.md) - - [All Together](./async-chat/all_together.md) + - [A broker as a connection point](./async-chat/connecting_readers_and_writers.md) + - [Glueing all together](./async-chat/all_together.md) - [Clean Shutdown](./async-chat/clean_shutdown.md) - [Handling Disconnection](./async-chat/handling_disconnection.md) - [Final Server Code](./async-chat/final_server_code.md) diff --git a/exercise-book/src/async-chat/all_together.md b/exercise-book/src/async-chat/all_together.md index 5e8b88c7..d6f494e9 100644 --- a/exercise-book/src/async-chat/all_together.md +++ b/exercise-book/src/async-chat/all_together.md @@ -1,6 +1,8 @@ -## All Together +## Gluing all together -At this point, we only need to start the broker to get a fully-functioning (in the happy case!) chat: +At this point, we only need to start the broker to get a fully-functioning (in the happy case!) chat. + +Scroll past the example find a list of all changes. ```rust # extern crate tokio; @@ -20,8 +22,8 @@ type Result = std::result::Result type Sender = mpsc::UnboundedSender; type Receiver = mpsc::UnboundedReceiver; -// main -async fn run() -> Result<()> { +#[tokio::main] +pub(crate) async fn main() -> Result<()> { accept_loop("127.0.0.1:8080").await } @@ -38,8 +40,8 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { Ok(()) } -async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { - let (reader, writer) = stream.into_split(); // 2 +async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { // 2 + let (reader, writer) = stream.into_split(); // 3 let reader = BufReader::new(reader); let mut lines = reader.lines(); @@ -56,26 +58,30 @@ async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> name: name.clone(), stream: writer, }) - .unwrap(); // 3 - - while let Ok(Some(line)) = lines.next_line().await { - let (dest, msg) = match line.find(':') { - None => continue, - Some(idx) => (&line[..idx], line[idx + 1..].trim()), - }; - let dest: Vec = dest - .split(',') - .map(|name| name.trim().to_string()) - .collect(); - let msg: String = msg.trim().to_string(); - - broker - .send(Event::Message { // 4 - from: name.clone(), - to: dest, - msg, - }) - .unwrap(); + .unwrap(); // 5 + + loop { + if let Some(line) = lines.next_line().await? { + let (dest, msg) = match line.find(':') { + None => continue, + Some(idx) => (&line[..idx], line[idx + 1..].trim()), + }; + let dest: Vec = dest + .split(',') + .map(|name| name.trim().to_string()) + .collect(); + let msg: String = msg.trim().to_string(); + + broker + .send(Event::Message { // 4 + from: name.clone(), + to: dest, + msg, + }) + .unwrap(); + } else { + break; + } } Ok(()) @@ -83,10 +89,14 @@ async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> async fn connection_writer_loop( messages: &mut Receiver, - stream: &mut OwnedWriteHalf, + stream: &mut OwnedWriteHalf // 3 ) -> Result<()> { - while let Some(msg) = messages.recv().await { - stream.write_all(msg.as_bytes()).await?; + loop { + let msg = messages.recv().await; + match msg { + Some(msg) => stream.write_all(msg.as_bytes()).await?, + None => break, + } } Ok(()) } @@ -107,7 +117,11 @@ enum Event { async fn broker_loop(mut events: Receiver) { let mut peers: HashMap> = HashMap::new(); - while let Some(event) = events.recv().await { + loop { + let event = match events.recv().await { + Some(event) => event, + None => break, + }; match event { Event::Message { from, to, msg } => { for addr in to { @@ -144,7 +158,8 @@ where ``` 1. Inside the `accept_loop`, we create the broker's channel and `task`. -2. Inside `connection_loop`, we need to split the `TcpStream`, to be able to share it with the `connection_writer_loop`. -3. On login, we notify the broker. +2. We need the connection_loop to accept a handle to the broker. +3. Inside `connection_loop`, we need to split the `TcpStream`, to be able to share it with the `connection_writer_loop`. +4. On login, we notify the broker. Note that we `.unwrap` on send: broker should outlive all the clients and if that's not the case the broker probably panicked, so we can escalate the panic as well. -4. Similarly, we forward parsed messages to the broker, assuming that it is alive. +5. Similarly, we forward parsed messages to the broker, assuming that it is alive. diff --git a/exercise-book/src/async-chat/clean_shutdown.md b/exercise-book/src/async-chat/clean_shutdown.md index 085abdfb..761a1d3a 100644 --- a/exercise-book/src/async-chat/clean_shutdown.md +++ b/exercise-book/src/async-chat/clean_shutdown.md @@ -2,11 +2,15 @@ One of the problems of the current implementation is that it doesn't handle graceful shutdown. If we break from the accept loop for some reason, all in-flight tasks are just dropped on the floor. + +We will intercept `Ctrl-C`. + A more correct shutdown sequence would be: 1. Stop accepting new clients -2. Deliver all pending messages -3. Exit the process +2. Notify the readers we're not accepting new messages +3. Deliver all pending messages +4. Exit the process A clean shutdown in a channel based architecture is easy, although it can appear a magic trick at first. In Rust, receiver side of a channel is closed as soon as all senders are dropped. @@ -21,7 +25,40 @@ However, we never wait for broker and writers, which might cause some messages t We also need to notify all readers that we are going to stop accepting messages. Here, we use `tokio::sync::Notify`. -Let's add waiting to the server: +Let's first add the notification feature to the readers. +We have to start using `select!` here to work +```rust +async fn connection_loop(broker: Sender, stream: TcpStream, shutdown: Arc) -> Result<()> { + // ... + loop { + tokio::select! { + Ok(Some(line)) = lines.next_line() => { + let (dest, msg) = match line.split_once(':') { + + None => continue, + Some((dest, msg)) => (dest, msg.trim()), + }; + let dest: Vec = dest + .split(',') + .map(|name| name.trim().to_string()) + .collect(); + let msg: String = msg.trim().to_string(); + + broker + .send(Event::Message { + from: name.clone(), + to: dest, + msg, + }) + .unwrap(); + }, + _ = shutdown.notified() => break, + } + } +} +``` + +Let's add Ctrl-C handling and waiting to the server. ```rust # extern crate tokio; @@ -68,15 +105,18 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let broker = task::spawn(broker_loop(broker_receiver)); let shutdown_notification = Arc::new(Notify::new()); - loop { - let (stream, _) = listener.accept().await?; - println!("Accepting from: {}", stream.peer_addr()?); - spawn_and_log_error(connection_loop(broker_sender.clone(), stream, shutdown_notification)); + tokio::select!{ + Ok((stream, _socket_addr)) = listener.accept() => { + println!("Accepting from: {}", stream.peer_addr()?); + spawn_and_log_error(connection_loop(broker_sender.clone(), stream, shutdown_notification.clone())); + }, + _ = tokio::signal::ctrl_c() => break, + } } println!("Shutting down server!"); - shutdown_notification.notify_waiters(); - drop(broker_sender); // 1 + shutdown_notification.notify_waiters(); // 1 + drop(broker_sender); // 2 broker.await?; // 5 Ok(()) } @@ -131,7 +171,11 @@ And to the broker: async fn broker_loop(mut events: Receiver) { let mut peers: HashMap> = HashMap::new(); - while let Some(event) = events.recv().await { + loop { + let event = match events.recv().await { + Some(event) => event, + None => break, + }; match event { Event::Message { from, to, msg } => { // ... @@ -156,11 +200,13 @@ async fn broker_loop(mut events: Receiver) { } ``` + Notice what happens with all of the channels once we exit the accept loop: -1. First, we drop the main broker's sender. +1. We notify all readers to stop accepting messages. +2. We drop the main broker's sender. That way when the readers are done, there's no sender for the broker's channel, and the channel closes. -2. Next, the broker exits `while let Some(event) = events.next().await` loop. +3. Next, the broker exits `while let Some(event) = events.next().await` loop. 3. It's crucial that, at this stage, we drop the `peers` map. This drops writer's senders. 4. Tokio will automatically wait for all finishing futures diff --git a/exercise-book/src/async-chat/connecting_readers_and_writers.md b/exercise-book/src/async-chat/connecting_readers_and_writers.md index 549a3ef6..1eb74794 100644 --- a/exercise-book/src/async-chat/connecting_readers_and_writers.md +++ b/exercise-book/src/async-chat/connecting_readers_and_writers.md @@ -1,13 +1,14 @@ -## Connecting Readers and Writers +## A broker as a connection point So how do we make sure that messages read in `connection_loop` flow into the relevant `connection_writer_loop`? We should somehow maintain a `peers: HashMap>` map which allows a client to find destination channels. However, this map would be a bit of shared mutable state, so we'll have to wrap an `RwLock` over it and answer tough questions of what should happen if the client joins at the same moment as it receives a message. -One trick to make reasoning about state simpler comes from the actor model. +One trick to make reasoning about state simpler is by taking inspiration from the actor model. We can create a dedicated broker task which owns the `peers` map and communicates with other tasks using channels. -By hiding `peers` inside such an "actor" task, we remove the need for mutexes and also make the serialization point explicit. +The broker reacts on events and appropriately informs the peers. +By hiding peer handling inside such an "actor" task, we remove the need for mutexes and also make the serialization point explicit. The order of events "Bob sends message to Alice" and "Alice joins" is determined by the order of the corresponding events in the broker's event queue. ```rust diff --git a/exercise-book/src/async-chat/final_server_code.md b/exercise-book/src/async-chat/final_server_code.md index 0544e408..811b8140 100644 --- a/exercise-book/src/async-chat/final_server_code.md +++ b/exercise-book/src/async-chat/final_server_code.md @@ -3,16 +3,16 @@ The final code looks like this: ```rust -# extern crate tokio; use std::{ collections::hash_map::{Entry, HashMap}, future::Future, + sync::Arc, }; use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, - sync::{mpsc, oneshot}, + sync::{mpsc, oneshot, Notify}, task, }; @@ -20,8 +20,8 @@ type Result = std::result::Result type Sender = mpsc::UnboundedSender; type Receiver = mpsc::UnboundedReceiver; -// main -async fn run() -> Result<()> { +#[tokio::main] +pub(crate) async fn main() -> Result<()> { accept_loop("127.0.0.1:8080").await } @@ -30,20 +30,29 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let (broker_sender, broker_receiver) = mpsc::unbounded_channel(); let broker = task::spawn(broker_loop(broker_receiver)); + let shutdown_notification = Arc::new(Notify::new()); - while let Ok((stream, _socket_addr)) = listener.accept().await { - println!("Accepting from: {}", stream.peer_addr()?); - spawn_and_log_error(connection_loop(broker_sender.clone(), stream)); + loop { + tokio::select!{ + Ok((stream, _socket_addr)) = listener.accept() => { + println!("Accepting from: {}", stream.peer_addr()?); + spawn_and_log_error(connection_loop(broker_sender.clone(), stream, shutdown_notification.clone())); + }, + _ = tokio::signal::ctrl_c() => break, + } } + println!("Shutting down!"); + shutdown_notification.notify_waiters(); drop(broker_sender); broker.await?; Ok(()) } -async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> { +async fn connection_loop(broker: Sender, stream: TcpStream, shutdown: Arc) -> Result<()> { let (reader, writer) = stream.into_split(); let reader = BufReader::new(reader); let mut lines = reader.lines(); + let (shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); let name = match lines.next_line().await { Ok(Some(line)) => line, @@ -53,7 +62,6 @@ async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> println!("user {} connected", name); - let (_shutdown_sender, shutdown_receiver) = oneshot::channel::<()>(); broker .send(Event::NewPeer { name: name.clone(), @@ -61,26 +69,34 @@ async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> shutdown: shutdown_receiver, }) .unwrap(); - - while let Ok(Some(line)) = lines.next_line().await { - let (dest, msg) = match line.find(':') { - None => continue, - Some(idx) => (&line[..idx], line[idx + 1..].trim()), - }; - let dest: Vec = dest - .split(',') - .map(|name| name.trim().to_string()) - .collect(); - let msg: String = msg.trim().to_string(); - - broker - .send(Event::Message { - from: name.clone(), - to: dest, - msg, - }) - .unwrap(); + + loop { + tokio::select! { + Ok(Some(line)) = lines.next_line() => { + let (dest, msg) = match line.split_once(':') { + + None => continue, + Some((dest, msg)) => (dest, msg.trim()), + }; + let dest: Vec = dest + .split(',') + .map(|name| name.trim().to_string()) + .collect(); + let msg: String = msg.trim().to_string(); + + broker + .send(Event::Message { + from: name.clone(), + to: dest, + msg, + }) + .unwrap(); + }, + _ = shutdown.notified() => break, + } } + println!("Closing connection loop!"); + drop(shutdown_sender); Ok(()) } @@ -99,6 +115,9 @@ async fn connection_writer_loop( _ = &mut shutdown => break } } + + println!("Closing connection_writer loop!"); + Ok(()) } @@ -118,18 +137,19 @@ enum Event { async fn broker_loop(mut events: Receiver) { let (disconnect_sender, mut disconnect_receiver) = - mpsc::unbounded_channel::<(String, Receiver)>(); // 1 + mpsc::unbounded_channel::<(String, Receiver)>(); let mut peers: HashMap> = HashMap::new(); loop { let event = tokio::select! { event = events.recv() => match event { - None => break, // 2 + None => break, Some(event) => event, }, disconnect = disconnect_receiver.recv() => { - let (name, _pending_messages) = disconnect.unwrap(); // 3 + let (name, _pending_messages) = disconnect.unwrap(); assert!(peers.remove(&name).is_some()); + println!("user {} disconnected", name); continue; }, }; @@ -138,7 +158,7 @@ async fn broker_loop(mut events: Receiver) { for addr in to { if let Some(peer) = peers.get_mut(&addr) { let msg = format!("from {}: {}\n", from, msg); - peer.send(msg).unwrap(); // 6 + peer.send(msg).unwrap(); } } } @@ -156,15 +176,16 @@ async fn broker_loop(mut events: Receiver) { let res = connection_writer_loop(&mut client_receiver, &mut stream, shutdown) .await; - disconnect_sender.send((name, client_receiver)).unwrap(); // 4 + println!("user {} disconnected", name); + disconnect_sender.send((name, client_receiver)).unwrap(); res }); } }, } } - drop(peers); // 5 - drop(disconnect_sender); // 6 + drop(peers); + drop(disconnect_sender); while let Some((_name, _pending_messages)) = disconnect_receiver.recv().await {} } @@ -178,14 +199,5 @@ where } }) } -``` -1. In the broker, we create a channel to reap disconnected peers and their undelivered messages. -2. The broker's main loop exits when the input events channel is exhausted (that is, when all readers exit). -3. Because broker itself holds a `disconnect_sender`, we know that the disconnections channel can't be fully drained in the main loop. -4. We send peer's name and pending messages to the disconnections channel in both the happy and the not-so-happy path. - Again, we can safely unwrap because the broker outlives writers. -5. We drop `peers` map to close writers' messages channel and shut down the writers for sure. - It is not strictly necessary in the current setup, where the broker waits for readers' shutdown anyway. - However, if we add a server-initiated shutdown (for example, kbd:[ctrl+c] handling), this will be a way for the broker to shutdown the writers. -6. Finally, we close and drain the disconnections channel. +``` diff --git a/exercise-book/src/async-chat/handling_disconnection.md b/exercise-book/src/async-chat/handling_disconnection.md index 05b7b0a2..32b911ed 100644 --- a/exercise-book/src/async-chat/handling_disconnection.md +++ b/exercise-book/src/async-chat/handling_disconnection.md @@ -86,24 +86,12 @@ In the `connection_writer_loop`, we now need to choose between shutdown and mess We use the `select` macro for this purpose: ```rust -# extern crate tokio; -# use std::future::Future; -# use tokio::{ -# io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, -# net::{tcp::OwnedWriteHalf, TcpListener, TcpStream, ToSocketAddrs}, -# sync::{mpsc, oneshot}, -# task, -# }; -# type Result = std::result::Result>; -# type Sender = mpsc::UnboundedSender; -# type Receiver = mpsc::UnboundedReceiver; -# async fn connection_writer_loop( messages: &mut Receiver, stream: &mut OwnedWriteHalf, - mut shutdown: oneshot::Receiver<()>, // 1 + mut shutdown: oneshot::Receiver<()>, ) -> Result<()> { - loop { // 2 + loop { tokio::select! { msg = messages.recv() => match msg { Some(msg) => stream.write_all(msg.as_bytes()).await?, @@ -112,6 +100,9 @@ async fn connection_writer_loop( _ = &mut shutdown => break } } + + println!("Closing connection_writer loop!"); + Ok(()) } ``` diff --git a/exercise-book/src/async-chat/receiving_messages.md b/exercise-book/src/async-chat/receiving_messages.md index 75decd47..b140ac7d 100644 --- a/exercise-book/src/async-chat/receiving_messages.md +++ b/exercise-book/src/async-chat/receiving_messages.md @@ -7,6 +7,8 @@ We need to: 2. interpret the first line as a login 3. parse the rest of the lines as a `login: message` +We highly recommend to go past this quick, this is a lot of protocol minutia. + ```rust # extern crate tokio; # use std::{ @@ -24,9 +26,10 @@ We need to: # async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let listener = TcpListener::bind(addr).await?; - while let Ok((stream, _socket_addr)) = listener.accept().await { + loop { + let (stream, _socket_addr) = listener.accept().await?; println!("Accepting from: {}", stream.peer_addr()?); - let _handle = task::spawn(connection_loop(stream)); // 1 + let _handle = task::spawn(connection_loop(stream)); } Ok(()) } @@ -56,7 +59,7 @@ async fn connection_loop(stream: TcpStream) -> Result<()> { .collect::>(); let msg = msg.to_string(); // TODO: this is temporary - println!("Received message:", msg); + println!("Received message: {}", msg); } else { break } diff --git a/exercise-book/src/async-chat/sending_messages.md b/exercise-book/src/async-chat/sending_messages.md index cee89a6f..54b04c66 100644 --- a/exercise-book/src/async-chat/sending_messages.md +++ b/exercise-book/src/async-chat/sending_messages.md @@ -28,7 +28,7 @@ type Receiver = mpsc::UnboundedReceiver; async fn connection_writer_loop( messages: &mut Receiver, - stream: &mut OwnedWriteHalf + stream: &mut OwnedWriteHalf // 3 ) -> Result<()> { loop { let msg = messages.recv().await; @@ -43,7 +43,7 @@ async fn connection_writer_loop( 1. We will use `mpsc` channels from `tokio`. 2. For simplicity, we will use `unbounded` channels, and won't be discussing backpressure in this tutorial. -3. As `connection_loop` and `connection_writer_loop` share the same `TcpStream`, we need to split it into a reader and a writer: +3. As `connection_loop` and `connection_writer_loop` share the same `TcpStream`, we use splitting. We'll glue this together later. ```rust # extern crate tokio; diff --git a/exercise-solutions/async-chat/src/client.rs b/exercise-solutions/async-chat/src/client.rs index a4894384..0fd8844d 100644 --- a/exercise-solutions/async-chat/src/client.rs +++ b/exercise-solutions/async-chat/src/client.rs @@ -35,6 +35,6 @@ async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { } } - println!("Client disconnected! Hit enter to quit."); + println!("Server disconnected! Hit enter to quit."); Ok(()) } diff --git a/exercise-templates/async-chat/step1/src/client.rs b/exercise-templates/async-chat/step1/src/client.rs index 3fb5b098..0fd8844d 100644 --- a/exercise-templates/async-chat/step1/src/client.rs +++ b/exercise-templates/async-chat/step1/src/client.rs @@ -12,12 +12,9 @@ pub(crate) async fn main() -> Result<()> { async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { let stream = TcpStream::connect(addr).await?; - let (reader, mut writer) = tokio::io::split(stream); - let reader = BufReader::new(reader); - let mut lines_from_server = reader.lines(); - - let stdin = BufReader::new(stdin()); - let mut lines_from_stdin = stdin.lines(); + let (reader, mut writer) = stream.into_split(); + let mut lines_from_server = BufReader::new(reader).lines(); + let mut lines_from_stdin = BufReader::new(stdin()).lines(); loop { tokio::select! { line = lines_from_server.next_line() => match line { @@ -37,5 +34,7 @@ async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { } } } + + println!("Server disconnected! Hit enter to quit."); Ok(()) } diff --git a/exercise-templates/async-chat/step2/src/client.rs b/exercise-templates/async-chat/step2/src/client.rs index 3fb5b098..0fd8844d 100644 --- a/exercise-templates/async-chat/step2/src/client.rs +++ b/exercise-templates/async-chat/step2/src/client.rs @@ -12,12 +12,9 @@ pub(crate) async fn main() -> Result<()> { async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { let stream = TcpStream::connect(addr).await?; - let (reader, mut writer) = tokio::io::split(stream); - let reader = BufReader::new(reader); - let mut lines_from_server = reader.lines(); - - let stdin = BufReader::new(stdin()); - let mut lines_from_stdin = stdin.lines(); + let (reader, mut writer) = stream.into_split(); + let mut lines_from_server = BufReader::new(reader).lines(); + let mut lines_from_stdin = BufReader::new(stdin()).lines(); loop { tokio::select! { line = lines_from_server.next_line() => match line { @@ -37,5 +34,7 @@ async fn try_main(addr: impl ToSocketAddrs) -> Result<()> { } } } + + println!("Server disconnected! Hit enter to quit."); Ok(()) } From 0c3fbe62745bd690636d4215d28a93b0178ae189 Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Tue, 28 May 2024 06:32:05 +0000 Subject: [PATCH 33/36] Make rustfmt happy --- exercise-solutions/async-chat/src/server.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/exercise-solutions/async-chat/src/server.rs b/exercise-solutions/async-chat/src/server.rs index b2491006..3e0e6902 100644 --- a/exercise-solutions/async-chat/src/server.rs +++ b/exercise-solutions/async-chat/src/server.rs @@ -28,7 +28,7 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { let shutdown_notification = Arc::new(Notify::new()); loop { - tokio::select!{ + tokio::select! { Ok((stream, _socket_addr)) = listener.accept() => { println!("Accepting from: {}", stream.peer_addr()?); spawn_and_log_error(connection_loop(broker_sender.clone(), stream, shutdown_notification.clone())); @@ -43,7 +43,11 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { Ok(()) } -async fn connection_loop(broker: Sender, stream: TcpStream, shutdown: Arc) -> Result<()> { +async fn connection_loop( + broker: Sender, + stream: TcpStream, + shutdown: Arc, +) -> Result<()> { let (reader, writer) = stream.into_split(); let reader = BufReader::new(reader); let mut lines = reader.lines(); @@ -64,7 +68,7 @@ async fn connection_loop(broker: Sender, stream: TcpStream, shutdown: Arc shutdown: shutdown_receiver, }) .unwrap(); - + loop { tokio::select! { Ok(Some(line)) = lines.next_line() => { @@ -78,7 +82,7 @@ async fn connection_loop(broker: Sender, stream: TcpStream, shutdown: Arc .map(|name| name.trim().to_string()) .collect(); let msg: String = msg.trim().to_string(); - + broker .send(Event::Message { from: name.clone(), From 476be960bf4402a6fb72ca8f3319eae2e5a5c779 Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Tue, 28 May 2024 06:40:45 +0000 Subject: [PATCH 34/36] Make rustfmt even more happy --- exercise-templates/async-chat/step1/src/server.rs | 6 ++++-- exercise-templates/async-chat/step2/src/server.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/exercise-templates/async-chat/step1/src/server.rs b/exercise-templates/async-chat/step1/src/server.rs index e6765e53..340e546e 100644 --- a/exercise-templates/async-chat/step1/src/server.rs +++ b/exercise-templates/async-chat/step1/src/server.rs @@ -21,10 +21,12 @@ pub(crate) async fn main() -> Result<()> { accept_loop("127.0.0.1:8080").await } -async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { // 1 +async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { + // 1 let listener = TcpListener::bind(addr).await?; // 2 - loop { // 3 + loop { + // 3 let (stream, _) = listener.accept().await?; // TODO } diff --git a/exercise-templates/async-chat/step2/src/server.rs b/exercise-templates/async-chat/step2/src/server.rs index e962164d..6d808508 100644 --- a/exercise-templates/async-chat/step2/src/server.rs +++ b/exercise-templates/async-chat/step2/src/server.rs @@ -63,7 +63,7 @@ async fn connection_loop(stream: TcpStream) -> Result<()> { async fn connection_writer_loop( messages: &mut Receiver, - stream: &mut OwnedWriteHalf + stream: &mut OwnedWriteHalf, ) -> Result<()> { loop { let msg = messages.recv().await; From f120144560f48b126f5ac13e15fefcf545ec2efd Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Tue, 28 May 2024 06:48:21 +0000 Subject: [PATCH 35/36] Switch minimal Rust version to 2021 --- exercise-book/book.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/exercise-book/book.toml b/exercise-book/book.toml index 485cb057..943d5a12 100644 --- a/exercise-book/book.toml +++ b/exercise-book/book.toml @@ -8,6 +8,9 @@ title = "Rust Exercises" [preprocessor.mermaid] command = "mdbook-mermaid" +[rust] +edition = "2021" + [output.html.playground] copyable = true # include the copy button for copying code snippets editable = true # allow the code editor to work From 23de07a2c88b931efda85a746d157b7e6320751b Mon Sep 17 00:00:00 2001 From: Florian Gilcher Date: Tue, 28 May 2024 06:57:06 +0000 Subject: [PATCH 36/36] Disable all tests for the async workbook --- exercise-book/src/async-chat/accept_loop.md | 6 +++--- exercise-book/src/async-chat/all_together.md | 2 +- exercise-book/src/async-chat/clean_shutdown.md | 6 +++--- .../src/async-chat/connecting_readers_and_writers.md | 2 +- exercise-book/src/async-chat/final_server_code.md | 3 +-- exercise-book/src/async-chat/handling_disconnection.md | 9 +++++---- exercise-book/src/async-chat/implementing_a_client.md | 2 +- exercise-book/src/async-chat/receiving_messages.md | 6 +++--- exercise-book/src/async-chat/sending_messages.md | 4 ++-- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/exercise-book/src/async-chat/accept_loop.md b/exercise-book/src/async-chat/accept_loop.md index 3cda7556..79d1b2a5 100644 --- a/exercise-book/src/async-chat/accept_loop.md +++ b/exercise-book/src/async-chat/accept_loop.md @@ -4,7 +4,7 @@ Let's implement the scaffold of the server: a loop that binds a TCP socket to an First of all, let's add required import boilerplate: -```rust +```rust,ignore # extern crate tokio; use std::future::Future; // 1 use tokio::{ @@ -27,7 +27,7 @@ type Result = std::result::Result Now we can write the server's accept loop: -```rust +```rust,ignore # extern crate tokio; # use tokio::net::{TcpListener, ToSocketAddrs}; # type Result = std::result::Result>; @@ -52,7 +52,7 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { // 1 Finally, let's add main: -```rust +```rust,ignore # extern crate tokio; # use tokio::net::{ToSocketAddrs}; # type Result = std::result::Result>; diff --git a/exercise-book/src/async-chat/all_together.md b/exercise-book/src/async-chat/all_together.md index d6f494e9..8bf234f5 100644 --- a/exercise-book/src/async-chat/all_together.md +++ b/exercise-book/src/async-chat/all_together.md @@ -4,7 +4,7 @@ At this point, we only need to start the broker to get a fully-functioning (in t Scroll past the example find a list of all changes. -```rust +```rust,ignore # extern crate tokio; use std::{ collections::hash_map::{Entry, HashMap}, diff --git a/exercise-book/src/async-chat/clean_shutdown.md b/exercise-book/src/async-chat/clean_shutdown.md index 761a1d3a..1e8998c4 100644 --- a/exercise-book/src/async-chat/clean_shutdown.md +++ b/exercise-book/src/async-chat/clean_shutdown.md @@ -27,7 +27,7 @@ We also need to notify all readers that we are going to stop accepting messages. Let's first add the notification feature to the readers. We have to start using `select!` here to work -```rust +```rust,ignore async fn connection_loop(broker: Sender, stream: TcpStream, shutdown: Arc) -> Result<()> { // ... loop { @@ -60,7 +60,7 @@ async fn connection_loop(broker: Sender, stream: TcpStream, shutdown: Arc Let's add Ctrl-C handling and waiting to the server. -```rust +```rust,ignore # extern crate tokio; # use std::{ # collections::hash_map::{Entry, HashMap}, @@ -124,7 +124,7 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> { And to the broker: -```rust +```rust,ignore # extern crate tokio; # use std::{ # collections::hash_map::{Entry, HashMap}, diff --git a/exercise-book/src/async-chat/connecting_readers_and_writers.md b/exercise-book/src/async-chat/connecting_readers_and_writers.md index 1eb74794..36f948c2 100644 --- a/exercise-book/src/async-chat/connecting_readers_and_writers.md +++ b/exercise-book/src/async-chat/connecting_readers_and_writers.md @@ -11,7 +11,7 @@ The broker reacts on events and appropriately informs the peers. By hiding peer handling inside such an "actor" task, we remove the need for mutexes and also make the serialization point explicit. The order of events "Bob sends message to Alice" and "Alice joins" is determined by the order of the corresponding events in the broker's event queue. -```rust +```rust,ignore # extern crate tokio; # use std::future::Future; # use tokio::{ diff --git a/exercise-book/src/async-chat/final_server_code.md b/exercise-book/src/async-chat/final_server_code.md index 811b8140..ad18ec94 100644 --- a/exercise-book/src/async-chat/final_server_code.md +++ b/exercise-book/src/async-chat/final_server_code.md @@ -2,7 +2,7 @@ The final code looks like this: -```rust +```rust,ignore use std::{ collections::hash_map::{Entry, HashMap}, future::Future, @@ -199,5 +199,4 @@ where } }) } - ``` diff --git a/exercise-book/src/async-chat/handling_disconnection.md b/exercise-book/src/async-chat/handling_disconnection.md index 32b911ed..28b071c1 100644 --- a/exercise-book/src/async-chat/handling_disconnection.md +++ b/exercise-book/src/async-chat/handling_disconnection.md @@ -17,7 +17,7 @@ This way, we statically guarantee that we issue shutdown exactly once, even if w First, let's add a shutdown channel to the `connection_loop`: -```rust +```rust,ignore # extern crate tokio; # use std::future::Future; # use tokio::{ @@ -85,7 +85,7 @@ async fn connection_loop(broker: Sender, stream: TcpStream) -> Result<()> In the `connection_writer_loop`, we now need to choose between shutdown and message channels. We use the `select` macro for this purpose: -```rust +```rust,ignore async fn connection_writer_loop( messages: &mut Receiver, stream: &mut OwnedWriteHalf, @@ -117,7 +117,7 @@ The final thing to handle is actually clean up our peers map. Here, we need to e To not lose these messages completely, we'll return the writers messages receiver back to the broker. This also allows us to establish a useful invariant that the message channel strictly outlives the peer in the peers map, and makes the broker itself infallible. -```rust +```rust,ignore async fn broker_loop(mut events: Receiver) { let (disconnect_sender, mut disconnect_receiver) = mpsc::unbounded_channel::<(String, Receiver)>(); // 1 @@ -163,4 +163,5 @@ async fn broker_loop(mut events: Receiver) { drop(peers); drop(disconnect_sender); while let Some((_name, _pending_messages)) = disconnect_receiver.recv().await {} -} \ No newline at end of file +} +``` \ No newline at end of file diff --git a/exercise-book/src/async-chat/implementing_a_client.md b/exercise-book/src/async-chat/implementing_a_client.md index 6db58a4d..790d2e98 100644 --- a/exercise-book/src/async-chat/implementing_a_client.md +++ b/exercise-book/src/async-chat/implementing_a_client.md @@ -12,7 +12,7 @@ Programming this with threads is cumbersome, especially when implementing a clea With async, the `select!` macro is all that is needed. -```rust +```rust,ignore # extern crate tokio; use tokio::{ io::{stdin, AsyncBufReadExt, AsyncWriteExt, BufReader}, diff --git a/exercise-book/src/async-chat/receiving_messages.md b/exercise-book/src/async-chat/receiving_messages.md index b140ac7d..100259fd 100644 --- a/exercise-book/src/async-chat/receiving_messages.md +++ b/exercise-book/src/async-chat/receiving_messages.md @@ -9,7 +9,7 @@ We need to: We highly recommend to go past this quick, this is a lot of protocol minutia. -```rust +```rust,ignore # extern crate tokio; # use std::{ # collections::hash_map::{Entry, HashMap}, @@ -87,7 +87,7 @@ One serious problem in the above solution is that, while we correctly propagate That is, `task::spawn` does not return an error immediately (it can't, it needs to run the future to completion first), only after it is joined. We can "fix" it by waiting for the task to be joined, like this: -```rust +```rust,ignore # extern crate tokio; # use tokio::{ # net::TcpStream, @@ -114,7 +114,7 @@ That is, a flaky internet connection of one peer brings down the whole chat room A correct way to handle client errors in this case is log them, and continue serving other clients. So let's use a helper function for this: -```rust +```rust,ignore # extern crate tokio; # use std::future::Future; # use tokio::task; diff --git a/exercise-book/src/async-chat/sending_messages.md b/exercise-book/src/async-chat/sending_messages.md index 54b04c66..3f3b7153 100644 --- a/exercise-book/src/async-chat/sending_messages.md +++ b/exercise-book/src/async-chat/sending_messages.md @@ -6,7 +6,7 @@ This way, we also have compartmentalised that activity and automatically seriali So let's create a `connection_writer_loop` task which receives messages over a channel and writes them to the socket. If Alice and Charley send two messages to Bob at the same time, Bob will see the messages in the same order as they arrive in the channel. -```rust +```rust,ignore # extern crate tokio; # use std::{ # collections::hash_map::{Entry, HashMap}, @@ -45,7 +45,7 @@ async fn connection_writer_loop( 2. For simplicity, we will use `unbounded` channels, and won't be discussing backpressure in this tutorial. 3. As `connection_loop` and `connection_writer_loop` share the same `TcpStream`, we use splitting. We'll glue this together later. - ```rust + ```rust,ignore # extern crate tokio; # use tokio::net::TcpStream; # async fn connection_loop(stream: TcpStream) {