1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-03-22 14:59:37 +02:00

Add a new async exercise: elevator simulation. ()

This commit is contained in:
Dustin J. Mitchell 2023-04-13 10:26:57 -04:00 committed by GitHub
parent d8e442b9cb
commit 6c97e1a7be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 743 additions and 0 deletions

@ -226,6 +226,10 @@
- [With Java](android/interoperability/java.md)
- [Exercises](exercises/day-4/android.md)
# Async (temp until issue181 is closed)
- [Exercises](exercises/day-4/elevator.md)
# Final Words
- [Thanks!](thanks.md)

@ -0,0 +1 @@
# Exercises

@ -0,0 +1,91 @@
# 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.

318
src/exercises/day-4/elevator/Cargo.lock generated Normal file

@ -0,0 +1,318 @@
# 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"

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

@ -0,0 +1,234 @@
//! 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);
}
}
}

@ -0,0 +1,28 @@
//! 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();
}
_ => {}
}
}
}

@ -0,0 +1,35 @@
//! 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();
}

@ -0,0 +1,23 @@
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);
}
}