Fix a-chat tutorial issues (#573)

* tutorial/receiving_messages: fix future output type bound

* tutorial/receiving_messages: remove unneeded message trimming

Trimming was done twice on messages, so one of the two instances can
be removed. I personally think removing the first instance, in which
we are splitting names from messages makes the code more readable
than removing the second instance, but other examples further in
the tutorial show the second instance removed.

* tutorial/receiving_messages: declare use of TcpStream and io::BufReader

Readers couldn't see the `use` lines corresponding to these two
structures.

* tutorial/connecting_readers_and_writers: typos and grammar fixes

* tutorial/all_together: remove unneeded use async_std::io

* tutorial: use SinkExt consistently from futures::sink::SinkExt

* tutorial/handling_disconnection: hide mpsc use clause and remove empty lines

The empty lines translate to the output making it look weird.

* tutorial/handling_disconnection: fix typos

* tutorial/handling_disconnection: use ? in broker_handle.await

We were happy to return an Err variant from the broker_handle before
and nothing has changed in this regard, so bubbling it up to run().
pull/561/head
Alejandro Martinez Ruiz 5 years ago committed by Stjepan Glavina
parent b3d30de4a1
commit ba1ee2d204

@ -6,13 +6,13 @@ At this point, we only need to start the broker to get a fully-functioning (in t
# extern crate async_std;
# extern crate futures;
use async_std::{
io::{self, BufReader},
io::BufReader,
net::{TcpListener, TcpStream, ToSocketAddrs},
prelude::*,
task,
};
use futures::channel::mpsc;
use futures::SinkExt;
use futures::sink::SinkExt;
use std::{
collections::hash_map::{HashMap, Entry},
sync::Arc,

@ -30,7 +30,7 @@ Let's add waiting to the server:
# task,
# };
# use futures::channel::mpsc;
# use futures::SinkExt;
# use futures::sink::SinkExt;
# use std::{
# collections::hash_map::{HashMap, Entry},
# sync::Arc,
@ -163,7 +163,7 @@ And to the broker:
# task,
# };
# use futures::channel::mpsc;
# use futures::SinkExt;
# use futures::sink::SinkExt;
# use std::{
# collections::hash_map::{HashMap, Entry},
# sync::Arc,

@ -2,12 +2,12 @@
## 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 an `peers: HashMap<String, Sender<String>>` map which allows a client to find destination channels.
We should somehow maintain a `peers: HashMap<String, Sender<String>>` 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 tasks which owns the `peers` map and communicates with other tasks by channels.
By hiding `peers` inside such an "actor" task, we remove the need for mutxes and also make serialization point explicit.
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
@ -92,9 +92,9 @@ async fn broker_loop(mut events: Receiver<Event>) -> Result<()> {
}
```
1. Broker should handle two types of events: a message or an arrival of a new peer.
2. Internal state of the broker is a `HashMap`.
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 new peer, we first register it in the peer's map ...
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.

@ -22,7 +22,7 @@ First, let's add a shutdown channel to the `connection_loop`:
# extern crate futures;
# use async_std::net::TcpStream;
# use futures::channel::mpsc;
# use futures::SinkExt;
# use futures::sink::SinkExt;
# use std::sync::Arc;
#
# type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
@ -60,8 +60,8 @@ async fn connection_loop(mut broker: Sender<Event>, stream: Arc<TcpStream>) -> R
}
```
1. To enforce that no messages are send along the shutdown channel, we use an uninhabited type.
2. We pass the shutdown channel to the writer task
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.
@ -71,14 +71,12 @@ We use the `select` macro for this purpose:
# extern crate async_std;
# extern crate futures;
# use async_std::{net::TcpStream, prelude::*};
use futures::channel::mpsc;
# use futures::channel::mpsc;
use futures::{select, FutureExt};
# use std::sync::Arc;
# type Receiver<T> = mpsc::UnboundedReceiver<T>;
# type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
# type Sender<T> = mpsc::UnboundedSender<T>;
# #[derive(Debug)]
# enum Void {} // 1
@ -112,7 +110,7 @@ async fn connection_writer_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 infailable.
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
@ -128,7 +126,8 @@ use async_std::{
task,
};
use futures::channel::mpsc;
use futures::{select, FutureExt, SinkExt};
use futures::sink::SinkExt;
use futures::{select, FutureExt};
use std::{
collections::hash_map::{Entry, HashMap},
future::Future,
@ -158,7 +157,7 @@ async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> {
spawn_and_log_error(connection_loop(broker_sender.clone(), stream));
}
drop(broker_sender);
broker_handle.await;
broker_handle.await?;
Ok(())
}

@ -10,14 +10,18 @@ We need to:
```rust,edition2018
# extern crate async_std;
# use async_std::{
# io::BufReader,
# net::{TcpListener, TcpStream, ToSocketAddrs},
# net::{TcpListener, ToSocketAddrs},
# prelude::*,
# task,
# };
#
# type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
#
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();
@ -46,7 +50,7 @@ async fn connection_loop(stream: TcpStream) -> Result<()> {
Some(idx) => (&line[..idx], line[idx + 1 ..].trim()),
};
let dest: Vec<String> = dest.split(',').map(|name| name.trim().to_string()).collect();
let msg: String = msg.trim().to_string();
let msg: String = msg.to_string();
}
Ok(())
}
@ -130,7 +134,7 @@ So let's use a helper function for this:
# };
fn spawn_and_log_error<F>(fut: F) -> task::JoinHandle<()>
where
F: Future<Output = io::Result<()>> + Send + 'static,
F: Future<Output = Result<()>> + Send + 'static,
{
task::spawn(async move {
if let Err(e) = fut.await {

Loading…
Cancel
Save