What is the difference between other_task_v1 and other_task_v2?
The behavior I want is that of other_task_v2: it waits on both channels and responds to each message that is received.
other_task_v1 on the other hand can only receive one message on each channel, and once both have been received the select statement immediately returns complete and the loop iterates. This is how the async-book presents select! in the first example here select! - Asynchronous Programming in Rust
so I was confused for a while why it wasn't working. It doesn't really explain how it will behave.
In terms of the code, the only difference I can see is the use of pin_mut! in v1 which is required. If pin_mut! is not the cause of the difference in behavior then it's somewhat confusing that code that looks referentially the same behaves so different (I know Rust is not really functional but I don't see any obvious "side effects" here).
Also is this the right way to structure code for what I want to do? In the real code, the first branch of the select statement is a websocket stream, and the second branch like this code is the same (an async channel). I want to continuously wait for events from the two sources and handle them as they arrive, without interrupting the other waiting branch.
The key difference between these two functions is the calls to recv(). One call to recv() produces one future; a future (that isn't offering something outside of the contract of Future) completes at most once; so each call to recv() corresponds to at most one received message.
In other_task_v1, once each one has provided a message, it's a defunct future that will do nothing. (If you weren't using fuse() — e.g. Tokio's version of select! does not require it — then the future would likely panic because you polled it again after it completed.)
In other_task_v2, you call recv() inside the loop, so you can receive multiple messages.
The pin_mut! is sort of a hint about the bug in your code: sometimes it is in fact necessary to explicitly pin futures, but in ordinary async code, the most common reason it is necessary when you are doing something like other_task_v1 on purpose. For example, you might have intended to, on the receipt of one message from recv_a, break the loop, but otherwise receive multiple messages from recv_b and process them. In that case, you would have to pin recv_a (because it is being polled across multiple loop iterations), but would not need to pin recv_b.
The primary difference is that, in v1, you call recv()outside of the loop — and so every select! within the loop uses the same recv futures, which become useless once they've resolved once — while v2 performs the calls inside the loop, and thus a new "receive" operation is attempted on each iteration. Also note that the Asynchronous Programming in Rust example you're basing v1 on doesn't have a loop.