1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-03-21 22:49:44 +02:00

Add the description of the chat-app exercise ()

* Adds a description of the async chat exercise
* Simplifies the use of Error in chat-async
* Links the solution to the async chat exercise
* Removes the elevator exercise
This commit is contained in:
rbehjati 2023-05-17 18:22:11 +01:00 committed by GitHub
parent 8406697449
commit 83663daaa2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 189 additions and 777 deletions

16
Cargo.lock generated

@ -187,7 +187,6 @@ version = "0.1.0"
dependencies = [
"futures-util",
"http",
"thiserror",
"tokio",
"tokio-websockets",
]
@ -1996,6 +1995,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "0.3.10"
@ -2185,7 +2193,9 @@ dependencies = [
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.48.0",
@ -2251,9 +2261,9 @@ dependencies = [
[[package]]
name = "tokio-websockets"
version = "0.3.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d009a9a2a71bd44791363f42ba2dd7d47bf49aa61f7c43eabcd29a285e75a865"
checksum = "8d1df20f31f428a351ec353699a82aad49360090e8f6320ac0dacf5def4f1573"
dependencies = [
"base64 0.21.0",
"bytes",

@ -52,6 +52,7 @@ editable = true
"exercises/day-4/link-checker.html" = "../concurrency/link-checker.html"
"exercises/day-4/morning.html" = "../concurrency/morning.html"
"exercises/day-4/solutions-morning.html" = "../concurrency/solutions-morning.html"
"exercises/concurrency/elevator.html" = "chat-app.html"
"generics/closures.html" = "../traits/closures.html"
"generics/impl-trait.html" = "../traits/impl-trait.html"
"generics/trait-bounds.html" = "../traits/trait-bounds.html"

@ -292,7 +292,7 @@
- [Async Traits](async/pitfalls/async-traits.md)
- [Exercises](exercises/concurrency/afternoon.md)
- [Dining Philosophers](exercises/concurrency/dining-philosophers-async.md)
- [Elevator Operations](exercises/concurrency/elevator.md)
- [Broadcast Chat Application](exercises/concurrency/chat-app.md)
# Final Words

@ -5,8 +5,8 @@ To practice your Async Rust skills, we have again two exercises for you:
* Dining philosophers: we already saw this problem in the morning. This time
you are going to implement it with Async Rust.
* The Elevator Problem: this is a larger project that allows you experiment
with more advanced Async Rust features and some of its pitfalls!
* A Broadcast Chat Application: this is a larger project that allows you
experiment with more advanced Async Rust features.
<details>

@ -0,0 +1,109 @@
# Broadcast Chat Application
In this exercise, we want to use our new knowledge to implement a broadcast
chat application. We have a chat server that the clients connect to and publish
their messages. The client reads user messages from the standard input, and
sends them to the server. The chat server broadcasts each message that it
receives to all the clients.
For this, we use [a broadcast channel][1] on the server, and
[`tokio_websockets`][2] for the communication between the client and the
server.
Create a new Cargo project and add the following dependencies:
`Cargo.toml`:
<!-- File Cargo.toml -->
```toml
{{#include chat-async/Cargo.toml}}
```
## The required APIs
You are going to need the following functions from `tokio` and
[`tokio_websockets`][2]. Spend a few minutes to familiarize yourself with the
API.
- [WebsocketStream::next()][3]: for asynchronously reading messages from a
Websocket Stream.
- [SinkExt::send()][4] implemented by `WebsocketStream`: for asynchronously
sending messages on a Websocket Stream.
- [BufReader::read_line()][5]: for asynchronously reading user messages
from the standard input.
- [Sender::subscribe()][6]: for subscribing to a broadcast channel.
## Two binaries
Normally in a Cargo project, you can have only one binary, and one
`src/main.rs` file. In this project, we need two binaries. One for the client,
and one for the server. You could potentially make them two separate Cargo
projects, but we are going to put them in a single Cargo project with two
binaries. For this to work, the client and the server code should go under
`src/bin` (see the [documentation][7]).
Copy the following server and client code into `src/bin/server.rs` and
`src/bin/client.rs`, respectively. Your task is to complete these files as
described below.
`src/bin/server.rs`:
<!-- File src/bin/server.rs -->
```rust,compile_fail
{{#include chat-async/src/bin/server.rs:setup}}
{{#include chat-async/src/bin/server.rs:handle_connection}}
// TODO: For a hint, see the description of the task below.
{{#include chat-async/src/bin/server.rs:main}}
```
`src/bin/client.rs`:
<!-- File src/bin/client.rs -->
```rust,compile_fail
{{#include chat-async/src/bin/client.rs:setup}}
// TODO: For a hint, see the description of the task below.
}
```
## Running the binaries
Run the server with:
```shell
$ cargo run --bin server
```
and the client with:
```shell
$ cargo run --bin client
```
## Tasks
* Implement the `handle_connection` function in `src/bin/server.rs`.
* Hint: Use `tokio::select!` for concurrently performing two tasks in a
continuous loop. One task receives messages from the client and broadcasts
them. The other sends messages received by the server to the client.
* Complete the main function in `src/bin/client.rs`.
* Hint: As before, use `tokio::select!` in a continuous loop for concurrently
performing two tasks: (1) reading user messages from standard input and
sending them to the server, and (2) receiving messages from the server, and
displaying them for the user.
* Optional: Once you are done, change the code to broadcast messages to all
clients, but the sender of the message.
[1]: https://docs.rs/tokio/latest/tokio/sync/broadcast/fn.channel.html
[2]: https://docs.rs/tokio-websockets/0.3.2/tokio_websockets/
[3]: https://docs.rs/tokio-websockets/0.3.2/tokio_websockets/proto/struct.WebsocketStream.html#method.next
[4]: https://docs.rs/futures-util/0.3.28/futures_util/sink/trait.SinkExt.html#method.send
[5]: https://docs.rs/tokio/latest/tokio/io/trait.AsyncBufReadExt.html#method.read_line
[6]: https://docs.rs/tokio/latest/tokio/sync/broadcast/struct.Sender.html#method.subscribe
[7]: https://doc.rust-lang.org/cargo/reference/cargo-targets.html#binaries

@ -6,6 +6,5 @@ edition = "2021"
[dependencies]
futures-util = "0.3.28"
http = "0.2.9"
thiserror = "1.0.40"
tokio = { version = "1.28.1", features = ["net", "macros", "time", "rt", "rt-multi-thread", "io-std", "io-util"] }
tokio-websockets = "0.3.0"
tokio = { version = "1.28.1", features = ["full"] }
tokio-websockets = "0.3.2"

@ -1,3 +1,18 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ANCHOR: setup
use futures_util::SinkExt;
use http::Uri;
use tokio::io::{AsyncBufReadExt, BufReader};
@ -12,6 +27,8 @@ async fn main() -> Result<(), tokio_websockets::Error> {
let stdin = tokio::io::stdin();
let mut stdin = BufReader::new(stdin);
// ANCHOR_END: setup
// Continuous loop for concurrently sending and receiving messages.
loop {
let mut line = String::new();
tokio::select! {

@ -1,46 +1,42 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ANCHOR: setup
use futures_util::sink::SinkExt;
use std::error::Error;
use std::net::SocketAddr;
use thiserror::Error;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast::error::{RecvError, SendError};
use tokio::sync::broadcast::{channel, Sender};
use tokio_websockets::{Message, ServerBuilder, WebsocketStream};
// ANCHOR_END: setup
#[derive(Error, Debug)]
enum ServerError {
#[error("websocket error: {0}")]
Websocket(String),
#[error("io error: {0}")]
IO(#[from] std::io::Error),
#[error("broadcast channel SendError: {0}")]
SendError(#[from] SendError<String>),
#[error("broadcast channel RecvError: {0}")]
RecvError(#[from] RecvError),
}
// tokio_websockets Error types do not implement std::error::Error, so we make do by just capturing
// the debug format for the error.
impl From<tokio_websockets::Error> for ServerError {
fn from(err: tokio_websockets::Error) -> Self {
ServerError::Websocket(format!("{:?}", err))
}
}
impl From<tokio_websockets::proto::ProtocolError> for ServerError {
fn from(err: tokio_websockets::proto::ProtocolError) -> Self {
ServerError::Websocket(format!("{:?}", err))
}
}
// ANCHOR: handle_connection
async fn handle_connection(
addr: SocketAddr,
mut ws_stream: WebsocketStream<TcpStream>,
bcast_tx: Sender<String>,
) -> Result<(), ServerError> {
) -> Result<(), Box<dyn Error + Send + Sync>> {
// ANCHOR_END: handle_connection
ws_stream
.send(Message::text("Welcome to chat! Type a message".into()))
.await?;
let mut bcast_rx = bcast_tx.subscribe();
// A continuous loop for concurrently performing two tasks: (1) receiving
// messages from `ws_stream` and broadcasting them, and (2) receiving
// messages on `bcast_rx` and sending them to the client.
loop {
tokio::select! {
incoming = ws_stream.next() => {
@ -59,10 +55,11 @@ async fn handle_connection(
}
}
}
// ANCHOR: main
}
#[tokio::main]
async fn main() -> Result<(), ServerError> {
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let (bcast_tx, _) = channel(16);
let listener = TcpListener::bind("127.0.0.1:2000").await?;
@ -80,3 +77,4 @@ async fn main() -> Result<(), ServerError> {
});
}
}
// ANCHOR_END: main

@ -1,91 +0,0 @@
# Elevator Operation
Elevators seem simple. You press a button, doors open, you wait, and you're at
the floor you requested. But implementing an elevator controller is surprisingly
difficult! This exercise involves building a simple elevator control that
operates in a simple simulator.
The overall design of this elevator uses the actor pattern: you will implement a
controller task that communicates with other components of the elevator system
by sending and receiving messages.
## Getting Started
Download the [exercise template](../../comprehensive-rust-exercises.zip) and look in the `elevator`
directory for the following files.
`src/main.rs`:
<!-- File src/main.rs -->
```rust,compile_fail
{{#include elevator/src/main.rs}}
```
`src/building.rs`:
<!-- File src/building.rs -->
```rust,compile_fail
{{#include elevator/src/building.rs}}
```
`src/driver.rs`:
<!-- File src/driver.rs -->
```rust,compile_fail
{{#include elevator/src/driver.rs}}
```
`src/controller.rs`:
<!-- File src/controller.rs -->
```rust,compile_fail
{{#include elevator/src/controller.rs}}
```
`Cargo.toml` (you shouldn't need to change this):
<!-- File Cargo.toml -->
```toml
{{#include elevator/Cargo.toml}}
```
Use `cargo run` to run the elevator simulation.
## Exercises
Begin by implementing a controller that can transport the passengers provided by
the simple driver. There is only one elevator, and passengers always go from
floor 0 to floor 2, one-by-one.
Once you have this done, make the problem more complex. Suggested tasks:
* Make the driver more complex, with passengers arriving at random floors with
random destinations at random times.
* Create a building with more than one elevator, and adjust the controller to
handle this efficiently.
* Add additional events and metadata to analyze your controller's efficiency.
What is the distribution of wait time for passengers? Is the result fair?
* Modify the building to support a maximum passenger capacity for each
elevator, and modify the controller to take this information into account.
* Update the driver to simulate business traffic, with lots of passengers going
up from the ground floor at the same time, and those passengers returning to
the ground floor some time later. Can your controller adjust to these
circumstances?
* Modify the building to support "destination dispatch", where passengers
signal their destination floor in the elevator lobby, before boarding the
elevator.
* If you are taking the course with other students, trade controllers or
drivers with another student to see how robust your design is.
* Build a textual or graphical display of the elevators as they run.

@ -1,318 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "elevator"
version = "0.1.0"
dependencies = [
"tokio",
]
[[package]]
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]]
name = "libc"
version = "0.2.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
[[package]]
name = "lock_api"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "mio"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys",
]
[[package]]
name = "num_cpus"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-sys",
]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]]
name = "proc-macro2"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "smallvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "syn"
version = "2.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001"
dependencies = [
"autocfg",
"bytes",
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"

@ -1,9 +0,0 @@
[workspace]
[package]
name = "elevator"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.26.0", features = ["full"] }

@ -1,234 +0,0 @@
//! The building simulates floors and elevators.
use tokio::sync::{broadcast, mpsc};
use tokio::task;
use tokio::time;
#[derive(Debug, Clone)]
pub enum Direction {
Up,
Down,
}
/// A passenger is a person with a destination floor in mind.
#[derive(Debug)]
struct Passenger {
destination: FloorId,
}
/// FloorId identifies a floor. These are zero-based integers.
pub type FloorId = usize;
/// Floor represents the current status of a floor in the building.
#[derive(Default, Debug)]
struct Floor {
passengers: Vec<Passenger>,
}
/// ElevatorId identifies an elevator in the building. These are zero-based integers.
pub type ElevatorId = usize;
/// Elevator represents the current status of an elevator in the building.
#[derive(Default, Debug)]
struct Elevator {
/// Floor the elevator is currently on. In the simulation the elevator
/// transports instantaneously from one floor to the next in a single
/// simulation tick.
position: FloorId,
/// Destination floor for the elevator, if any. This can change at any time.
destination: Option<FloorId>,
/// Passengers currently on the elevator.
passengers: Vec<Passenger>,
/// True if the elevator is stopped with the doors open. The elevator
/// will not move with the doors open, but they will close at the next
/// tick of the simulation.
doors_open: bool,
}
/// A BuildingEvent is an event that occurs in the building.
#[derive(Debug, Clone)]
pub enum BuildingEvent {
/// A passenger has pressed a floor button in the elevator.
FloorButtonPressed(ElevatorId, FloorId),
/// A passenger on the given floor has pressed the call button.
CallButtonPressed(FloorId, Direction),
/// The elevator has arrived at the given floor. If this is the
/// elevator's destination, then it will stop open its doors.
AtFloor(ElevatorId, FloorId),
/// A passenger has been delivered to their desired floor.
PassengerDelivered(FloorId),
}
/// A BuildingCommand tells the building what to do.
#[derive(Debug)]
pub enum BuildingCommand {
/// Set the elevator's destination. The elevator will close its doors
/// if necessary and then begin moving toward this floor.
GoToFloor(ElevatorId, FloorId),
}
/// A DriverCommand is a message from the driver to change the state of
/// the building.
#[derive(Debug)]
pub enum DriverCommand {
/// A passenger has arrived and is waiting for an elevator. The passenger will automatically
/// press the relevant call button, board the elevator when it arrives, press their floor
/// button, and depart when the doors open on their destination floor.
PassengerArrived { at: FloorId, destination: FloorId },
/// Halt all activity in the building and end the building task.
Halt,
}
/// Building manages the current status of the building.
#[derive(Debug)]
pub struct Building {
floors: Vec<Floor>,
elevators: Vec<Elevator>,
}
impl Building {
pub fn new(num_floors: usize, num_elevators: usize) -> Self {
let mut floors = vec![];
for _ in 0..num_floors {
floors.push(Floor::default());
}
let mut elevators = vec![];
for _ in 0..num_elevators {
elevators.push(Elevator::default());
}
Self { floors, elevators }
}
/// Start the building. The resulting channels are used to communicate
/// with the building
pub fn start(
self,
) -> (
task::JoinHandle<()>,
broadcast::Receiver<BuildingEvent>,
mpsc::Sender<BuildingCommand>,
mpsc::Sender<DriverCommand>,
) {
let (events_tx, events_rx) = broadcast::channel(10);
let (building_cmd_tx, building_cmd_rx) = mpsc::channel(10);
let (driver_cmd_tx, driver_cmd_rx) = mpsc::channel(10);
let task = tokio::spawn(self.run(events_tx, building_cmd_rx, driver_cmd_rx));
(task, events_rx, building_cmd_tx, driver_cmd_tx)
}
async fn run(
mut self,
events_tx: broadcast::Sender<BuildingEvent>,
mut building_cmd_rx: mpsc::Receiver<BuildingCommand>,
mut driver_cmd_rx: mpsc::Receiver<DriverCommand>,
) {
let mut ticker = time::interval(time::Duration::from_millis(100));
loop {
tokio::select! {
Some(BuildingCommand::GoToFloor(el, fl)) = building_cmd_rx.recv() => {
self.elevators[el].destination = Some(fl);
}
Some(cmd) = driver_cmd_rx.recv() => {
match cmd {
DriverCommand::PassengerArrived{at, destination} => {
self.new_passenger(&events_tx, at, destination).await;
}
DriverCommand::Halt => return,
}
}
_ = ticker.tick() => self.move_elevators(&events_tx).await
}
}
}
/// Move the elevators toward their destinations.
async fn move_elevators(&mut self, events_tx: &broadcast::Sender<BuildingEvent>) {
for el in 0..self.elevators.len() {
let elevator = &mut self.elevators[el];
// If the elevator's doors are open, close them and wait for the next tick.
if elevator.doors_open {
elevator.doors_open = false;
continue;
}
// If the elevator has somewhere to go, move toward it.
if let Some(dest) = elevator.destination {
if dest > elevator.position {
elevator.position += 1;
}
if dest < elevator.position {
elevator.position -= 1;
}
events_tx
.send(BuildingEvent::AtFloor(el, elevator.position))
.unwrap();
// If the elevator has reached its destination, open
// the doors and let passengers get on and off.
if elevator.position == dest {
elevator.destination = None;
elevator.doors_open = true;
self.exchange_passengers(&events_tx, el).await;
}
}
}
}
/// Handle a new passenger arriving at the given floor.
async fn new_passenger(
&mut self,
events_tx: &broadcast::Sender<BuildingEvent>,
at: FloorId,
destination: FloorId,
) {
println!("Passenger arrived at {} going to {}", at, destination);
if at == destination {
events_tx
.send(BuildingEvent::PassengerDelivered(destination))
.unwrap();
return;
}
self.floors[at].passengers.push(Passenger { destination });
let dir = if at < destination {
Direction::Up
} else {
Direction::Down
};
events_tx
.send(BuildingEvent::CallButtonPressed(at, dir))
.unwrap();
}
/// The doors for the given elevator are open, so take on and discharge passengers.
async fn exchange_passengers(
&mut self,
events_tx: &broadcast::Sender<BuildingEvent>,
el: ElevatorId,
) {
let elevator = &mut self.elevators[el];
let fl = elevator.position;
// Handle passengers leaving the elevator at their floor.
let (this_floor, other_floors): (Vec<Passenger>, Vec<Passenger>) = elevator
.passengers
.drain(..)
.partition(|px| px.destination == fl);
for px in this_floor {
events_tx
.send(BuildingEvent::PassengerDelivered(px.destination))
.unwrap();
}
elevator.passengers = other_floors;
// Handle passengers entering the elevator.
for px in self.floors[fl].passengers.drain(..) {
events_tx
.send(BuildingEvent::FloorButtonPressed(el, px.destination))
.unwrap();
elevator.passengers.push(px);
}
}
}

@ -1,28 +0,0 @@
//! The controller directs the elevators to operate so that passengers
//! get to their destinations.
use crate::building::{BuildingCommand, BuildingEvent};
use tokio::sync::{broadcast, mpsc};
pub async fn controller(
mut events_rx: broadcast::Receiver<BuildingEvent>,
building_cmd_tx: mpsc::Sender<BuildingCommand>,
) {
while let Ok(evt) = events_rx.recv().await {
match evt {
BuildingEvent::CallButtonPressed(at, _) => {
building_cmd_tx
.send(BuildingCommand::GoToFloor(0, at))
.await
.unwrap();
}
BuildingEvent::FloorButtonPressed(_, destination) => {
building_cmd_tx
.send(BuildingCommand::GoToFloor(0, destination))
.await
.unwrap();
}
_ => {}
}
}
}

@ -1,35 +0,0 @@
//! The driver controls when and where passengers arrive.
use crate::building::{Building, BuildingEvent, DriverCommand};
use tokio::sync::{broadcast, mpsc};
/// Create a new building to be driven by this driver.
pub fn make_building() -> Building {
Building::new(3, 1)
}
/// Simulate people arriving at the ground floor and going to the first floor, one by one.
pub async fn driver(
mut events_rx: broadcast::Receiver<BuildingEvent>,
driver_cmd_tx: mpsc::Sender<DriverCommand>,
) {
for _ in 0..3 {
// A passenger has arrived..
driver_cmd_tx
.send(DriverCommand::PassengerArrived {
at: 0,
destination: 2,
})
.await
.unwrap();
// Wait until they are delivered..
while let Ok(evt) = events_rx.recv().await {
if let BuildingEvent::PassengerDelivered(_) = evt {
break;
}
}
}
driver_cmd_tx.send(DriverCommand::Halt).await.unwrap();
}

@ -1,23 +0,0 @@
use building::BuildingEvent;
use tokio::sync::broadcast;
mod building;
mod controller;
mod driver;
#[tokio::main]
async fn main() {
let building = driver::make_building();
let (building_task, events_rx, building_cmd_tx, driver_cmd_tx) = building.start();
tokio::spawn(print_events(events_rx.resubscribe()));
tokio::spawn(driver::driver(events_rx.resubscribe(), driver_cmd_tx));
tokio::spawn(controller::controller(events_rx, building_cmd_tx));
building_task.await.unwrap();
}
async fn print_events(mut events_rx: broadcast::Receiver<BuildingEvent>) {
while let Ok(evt) = events_rx.recv().await {
println!("BuildingEvent::{:?}", evt);
}
}

@ -8,3 +8,19 @@
{{#include dining-philosophers-async.rs}}
```
## Broadcast Chat Application
([back to exercise](chat-app.md))
`src/bin/server.rs`:
```rust,compile_fail
{{#include chat-async/src/bin/server.rs}}
```
`src/bin/client.rs`:
```rust,compile_fail
{{#include chat-async/src/bin/client.rs}}
```