Using tokio channel to protect a resource (aka help with types and lifetimes still)

Hello. I got some pretty simple code...

#[derive(Debug)]
struct StatsdMessage<'a> {
    kind: &'a str,
    metric: &'a str,
    value: u64,
    tags: Option<Vec<&'a str>>,
    sample_rate: Option<f64>
}

#[tokio::main]
async fn main() {
    let (tx, mut rx) = tokio::sync::mpsc::channel(10);

    let tx1 = tx.clone();
    tokio::spawn(async move {
        let msg = StatsdMessage{
            kind: "count",
            value: 456,
            metric: "bar",
            tags: Some(vec!["blah:test"]),
            sample_rate: None
        };
        tx1.send(msg).await.unwrap();
    });

    while let Some(msg) = rx.recv().await {
        println!("{:?}", msg);
    }
}

Works as expected. But when I try to factor the body of the anonymous function out to a named function, I'm struggling with getting the types correct:

let tx1 = tx.clone();
tokio::spawn(async move {
    send_message(tx1).await.unwrap();
});

async fn send_message(tx: tokio::sync::mpsc::Sender<StatsdMessage>) {
    let msg = StatsdMessage{
        kind: "histogram",
        value: 123,
        metric: "foo",
        tags: Some(vec!["blah:test"]),
        sample_rate: None
    };
    tx.send(msg).await;
}

The compiler complains with this error:

error[E0726]: implicit elided lifetime not allowed here
  --> src/bin/statsd.rs:36:53
   |
36 | async fn send_message(tx: tokio::sync::mpsc::Sender<StatsdMessage>) {
   |                                                     ^^^^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

I tried as the compiler said, but I'm probably just getting syntax wrong. Can I get some help?

Also have some followup questions...

  1. What should the return type of the named function be?
  2. Anonymous functions are great; the type inference is a life saver! But when they get really big, it hurts maintainability... but if you break it out into named functions, you have to deal with the tedium of getting all the types right in the function signature. In my real world code, the function signature is massive and multiline because of all these giant type specs. What do people usually do?
  3. Also, I'm guessing I can't send a reference to a StatsdMessage over the channel because the task that spawned it will go out of scope and the reference would be invalid?

Thanks for the help!

Are you sure you want to be sending borrowed strings over a channel? What are the strings borrowed from - it would have to outlive both ends of the channel, which seems unlikely.

The code in your example uses just string literals, in which case you should avoid the lifetime parameters on the message type and just use fields of type &'static str.

If you are dynamically generating strings you should probably use fields of String instead of &str.

1 Like

Yup, string literals. Well, for most of the fields in the struct.

So I changed my struct to be like this:

struct StatsdMessage {
    kind: &'static str,
    metric: &'static str,
    value: u64,
    tags: Option<Vec<String>>,
    sample_rate: Option<f64>
}

But now filling out the tags doesn't feel ergonomic:

let msg = StatsdMessage{
    kind: "histogram",
    value: 123,
    metric: "foo",
    tags: Some(vec![String::from("blah:test")]),
    sample_rate: None
};

Is this just the price to pay?

Also, any tips on what the return value of the named function should be? Or the how to deal with massive function signatures when moving away from anonymous functions where you get all the really nice type inference?

Thanks!

This is the price to pay. Rust probably should have a better syntax sugar for this, but for now it doesn't.

Rust's borrowed/owned types are about memory management and ownership, and tracking the difference is essential for memory safety. You can't declare incorrect ownership for the sake of ergonomics.

"borrowed".into() is the shortest syntax for owned String where the type is known from context (it is here).

If you plan to mix literals and allocated strings, then Cow<'static, str> is also an option. It too needs .into() when assigning a value from a literal or String.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.