Rust State Machine Part 3
tldr
- I missed two things when learning from Gemini
- Send the confirmation last
- return
JoinHandlefor graceful shutdown - Part 3 code is here
- Every time
tokio::sync::mpsc::channel(32)is called a channel is created.- This gives you a pair of
(tx, rx)which links to each end of the channel - When
txis cloned you have multipletxto send to the same channel - In this example, the number of
while rx.recv().await {}loops matches the number of channels mpsc’srxcan not be cloned. Though somempmcimplementations allow you to clonerx
- This gives you a pair of
- The above means
- If you call
spawn_clienttwice you created two traffic light states, and they are separate - If you call
spawn_clientand clone the client/sender/remote, you created a remote control that could send commands to change the one and only traffic light state
- If you call
Overview
In the end of part 2, codex found two issues but my brain was a bit toasted so I decided to leave it and tackle later.
This post is me going through the issues and applying the fixes.
Issue 1: Sending Confirm Before State Change
The first issue is in the code in our tokio worker which tries to find out the next state based on the current state and command.
The reply was sent before the state changed
The actual state change happens when we put the state back into current_state, but before that the Ok(()) was already sent.
Issue 1 Fix
The solution is to separate deducing the state and setting the state and replying the channel. Make sure the channel reply is the last thing we do.
|
|
Issue 2: Not Gracefully Shutting Down
This is an issue where the threads/tokio tasks are not coordinating with the main thread.
In part2’s spawn_client there is a println!("all threads done"); after while let Some(cmd) = rx.recv().await {} breaks out.
This line is (1) not being accurate in the tokio version and (2) not guaranteed to be executed.
The while let Some(cmd) = rx.recv().await {} loop breaks out when all the senders are dropped and the existing queued messages are processed.
But in main.rs the drop of remote_control happens right before the main thread exits (i.e. the main program ends)
This means the line after while is racing against main shutting down. So println! could be ran if it won the race, or else the print message is never executed (because main shutting down won).
I didn’t realize how similar this is with goroutines, this is why in go code that needs to coordinate goroutines would use WaitGroup or a done channel to coordinate the goroutines.
Also another issue was the printed text is wrong, in the last part3 version each spawn_client actually spawns tokio workers. So the printed text should be println!("worker done");
Side Note: Difference Between Creating New Remotes vs Cloning Remotes
Client could create two clients by
- Calling
spawn_clienttwice, or - Call
spawn_clientonce andclone()the sender
There exists a big difference between these two.
(1) Is creating two channels and state machines, in other words it creates two traffic light systems.
(2) Is creating one channel and one state machine but creates a duplicated remote control that can control the same state (traffic light)
This matters because when you think about how many "worker done" would be printed.
Since calling spawn_client would create a worker (hence it’s while loop), we could say the number of "worker done" should be printed is supposed to be the number of workers created, which is how many times spawn_client was called.
Issue 2 Fix
To handle the coordinate issue we would return the JoinHandle from tokio::spawn() to main, and (1) make sure the client is dropped before exiting then (2) await on the returned JoinHandle
|
|
spawn_client also needs to return the JoinHandle
|
|
Do we need to manually drop the client in our example?
The answer to above is yes because otherwise there would be a deadlock.
- Worker is waiting for the
whileloop to end to signal JoinHandle - Main is blocked on JoinHandle but won’t drop the client
Another thing important to realize is, if you cloned remotes, all those remotes needs to be dropped or else a deadlock would happen.
Some After Thoughts
Learning from LLM
I am trying to use LLMs to learn things faster, but it is very hard to point out issues while learning a new thing on the fly. After thinking I think one of the solutions is to use it learn then find a way to verify then come back for some notes.
Self Retro, Why Did I Miss It
I didn’t realize that issue 2 was something I am very familiar in Go, it’s either setting up the WaitGroup or block on a channel waiting for the “Done” signal.
Then I was thinking how did I miss it.
I think this happened because I was in the process of learning how to use tokio and how to handle the state by workers. Also the complexity of Rust’s syntax might have contributed a bit. Or it might be I haven’t been doing goroutines for a while so I got Rusty.
Also I think I trusted LLMs a bit too much even in terms of learning.
See Also
- Rust State Machine Part 2
- Rust State Machine Part 1
- Map Flatmap and Options
- Rust Learning: Modules
- Rust Polars Parquet