From 83663daaa21bf20fa46a7a086cd5dac3303c4ef5 Mon Sep 17 00:00:00 2001 From: rbehjati Date: Wed, 17 May 2023 18:22:11 +0100 Subject: [PATCH] Add the description of the chat-app exercise (#641) * 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 --- Cargo.lock | 16 +- book.toml | 1 + src/SUMMARY.md | 2 +- src/exercises/concurrency/afternoon.md | 4 +- src/exercises/concurrency/chat-app.md | 109 ++++++ .../concurrency/chat-async/Cargo.toml | 5 +- .../concurrency/chat-async/src/bin/client.rs | 17 + .../concurrency/chat-async/src/bin/server.rs | 58 ++-- src/exercises/concurrency/elevator.md | 91 ----- src/exercises/concurrency/elevator/Cargo.lock | 318 ------------------ src/exercises/concurrency/elevator/Cargo.toml | 9 - .../concurrency/elevator/src/building.rs | 234 ------------- .../concurrency/elevator/src/controller.rs | 28 -- .../concurrency/elevator/src/driver.rs | 35 -- .../concurrency/elevator/src/main.rs | 23 -- .../concurrency/solutions-afternoon.md | 16 + 16 files changed, 189 insertions(+), 777 deletions(-) create mode 100644 src/exercises/concurrency/chat-app.md delete mode 100644 src/exercises/concurrency/elevator.md delete mode 100644 src/exercises/concurrency/elevator/Cargo.lock delete mode 100644 src/exercises/concurrency/elevator/Cargo.toml delete mode 100644 src/exercises/concurrency/elevator/src/building.rs delete mode 100644 src/exercises/concurrency/elevator/src/controller.rs delete mode 100644 src/exercises/concurrency/elevator/src/driver.rs delete mode 100644 src/exercises/concurrency/elevator/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 32ba8ce2..966ff4a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/book.toml b/book.toml index acb254d6..27169384 100644 --- a/book.toml +++ b/book.toml @@ -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" diff --git a/src/SUMMARY.md b/src/SUMMARY.md index d04259e6..9a12b02e 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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 diff --git a/src/exercises/concurrency/afternoon.md b/src/exercises/concurrency/afternoon.md index 3267a957..281c61e4 100644 --- a/src/exercises/concurrency/afternoon.md +++ b/src/exercises/concurrency/afternoon.md @@ -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.
diff --git a/src/exercises/concurrency/chat-app.md b/src/exercises/concurrency/chat-app.md new file mode 100644 index 00000000..e6307540 --- /dev/null +++ b/src/exercises/concurrency/chat-app.md @@ -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`: + + + +```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`: + + + +```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`: + + + +```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 diff --git a/src/exercises/concurrency/chat-async/Cargo.toml b/src/exercises/concurrency/chat-async/Cargo.toml index 62479f23..fd3ada98 100644 --- a/src/exercises/concurrency/chat-async/Cargo.toml +++ b/src/exercises/concurrency/chat-async/Cargo.toml @@ -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" diff --git a/src/exercises/concurrency/chat-async/src/bin/client.rs b/src/exercises/concurrency/chat-async/src/bin/client.rs index 0153fc1f..51aaf9f6 100644 --- a/src/exercises/concurrency/chat-async/src/bin/client.rs +++ b/src/exercises/concurrency/chat-async/src/bin/client.rs @@ -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! { diff --git a/src/exercises/concurrency/chat-async/src/bin/server.rs b/src/exercises/concurrency/chat-async/src/bin/server.rs index ee0eb0c4..9899b4bf 100644 --- a/src/exercises/concurrency/chat-async/src/bin/server.rs +++ b/src/exercises/concurrency/chat-async/src/bin/server.rs @@ -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), - #[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 for ServerError { - fn from(err: tokio_websockets::Error) -> Self { - ServerError::Websocket(format!("{:?}", err)) - } -} - -impl From 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, bcast_tx: Sender, -) -> Result<(), ServerError> { +) -> Result<(), Box> { + // 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> { 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 diff --git a/src/exercises/concurrency/elevator.md b/src/exercises/concurrency/elevator.md deleted file mode 100644 index 88b486a5..00000000 --- a/src/exercises/concurrency/elevator.md +++ /dev/null @@ -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`: - - - -```rust,compile_fail -{{#include elevator/src/main.rs}} -``` - -`src/building.rs`: - - - -```rust,compile_fail -{{#include elevator/src/building.rs}} -``` - -`src/driver.rs`: - - - -```rust,compile_fail -{{#include elevator/src/driver.rs}} -``` - -`src/controller.rs`: - - - -```rust,compile_fail -{{#include elevator/src/controller.rs}} -``` - -`Cargo.toml` (you shouldn't need to change this): - - - -```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. diff --git a/src/exercises/concurrency/elevator/Cargo.lock b/src/exercises/concurrency/elevator/Cargo.lock deleted file mode 100644 index a03c0f39..00000000 --- a/src/exercises/concurrency/elevator/Cargo.lock +++ /dev/null @@ -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" diff --git a/src/exercises/concurrency/elevator/Cargo.toml b/src/exercises/concurrency/elevator/Cargo.toml deleted file mode 100644 index f2c83877..00000000 --- a/src/exercises/concurrency/elevator/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[workspace] - -[package] -name = "elevator" -version = "0.1.0" -edition = "2021" - -[dependencies] -tokio = { version = "1.26.0", features = ["full"] } diff --git a/src/exercises/concurrency/elevator/src/building.rs b/src/exercises/concurrency/elevator/src/building.rs deleted file mode 100644 index 3aca0321..00000000 --- a/src/exercises/concurrency/elevator/src/building.rs +++ /dev/null @@ -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, -} - -/// 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, - /// Passengers currently on the elevator. - passengers: Vec, - /// 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, - elevators: Vec, -} - -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, - mpsc::Sender, - mpsc::Sender, - ) { - 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, - mut building_cmd_rx: mpsc::Receiver, - mut driver_cmd_rx: mpsc::Receiver, - ) { - 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) { - 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, - 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, - 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, Vec) = 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); - } - } -} diff --git a/src/exercises/concurrency/elevator/src/controller.rs b/src/exercises/concurrency/elevator/src/controller.rs deleted file mode 100644 index 855fa223..00000000 --- a/src/exercises/concurrency/elevator/src/controller.rs +++ /dev/null @@ -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, - building_cmd_tx: mpsc::Sender, -) { - 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(); - } - _ => {} - } - } -} diff --git a/src/exercises/concurrency/elevator/src/driver.rs b/src/exercises/concurrency/elevator/src/driver.rs deleted file mode 100644 index e9e07ad2..00000000 --- a/src/exercises/concurrency/elevator/src/driver.rs +++ /dev/null @@ -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, - driver_cmd_tx: mpsc::Sender, -) { - 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(); -} diff --git a/src/exercises/concurrency/elevator/src/main.rs b/src/exercises/concurrency/elevator/src/main.rs deleted file mode 100644 index d7162580..00000000 --- a/src/exercises/concurrency/elevator/src/main.rs +++ /dev/null @@ -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) { - while let Ok(evt) = events_rx.recv().await { - println!("BuildingEvent::{:?}", evt); - } -} diff --git a/src/exercises/concurrency/solutions-afternoon.md b/src/exercises/concurrency/solutions-afternoon.md index 52d57bf4..3ccf76b8 100644 --- a/src/exercises/concurrency/solutions-afternoon.md +++ b/src/exercises/concurrency/solutions-afternoon.md @@ -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}} +``` +