I don't understand why tx1.closed() doesn't get triggered in this async select code

⚓ Rust    📅 2025-08-21    👤 surdeus    👁️ 4      

surdeus

I asked this question to two different LLMs but they're bullshitting me with made-up explanations and non-functional hallucinated code. So I decided to try BI instead of "AI", or Brain Intelligence or plan B Intelligence.

When the first task holding its interior select! gets dropped early, I don't get the tx1.closed() branch get executed.

use std::time::Duration;

use tokio::sync::oneshot;

async fn some_operation() -> String {
    tokio::time::sleep(Duration::from_millis(500)).await;

    String::from("Some operation finished.")
}

#[tokio::main]
async fn main() {
    let (mut tx1, rx1) = oneshot::channel();
    let (tx2, rx2) = oneshot::channel();

    tokio::spawn(async {
        tokio::select! {
            val = some_operation() => {
                let _ = tx1.send(val);
            }
            _ = tx1.closed() => {
                    println!("Some operation cancelled");
                }
        }
    });

    tokio::spawn(async {
        tokio::time::sleep(Duration::from_millis(500)).await;
        let _ = tx2.send("two");
    });

    tokio::select! {
        val = rx1 => {
            println!("rx1 completed first with {:?}", val);
        }
        val = rx2 => {
            println!("rx2 completed first with {:?}", val);
        }
    }
}

Playground link

My guess is: When the first branch in main select wins, it's normal that the tx1.closed() doesn't get executed. When the second branch wins, then because the whole channel 1 with tx1 and rx1 gets dropped, everything in it is also dropped. And drops don't cascade inwards.

One question I have though is: What is the idiomatic way to get the tx1.closed() branch to also run at the drop event then? Without extra further polling happening ideally.

Thank you.

4 posts - 3 participants

Read full topic

🏷️ Rust_feed