Advice on lifetime usage with stateful values


#1

Hey all,

First time posting here - enjoying learning Rust thus far :slight_smile: I’d like some advice on the idiomatic way to structure applications with stateful values. I’m implementing a system that provides some gRPC APIs, using the grpcio crate. With this frame, I have some questions about lifetimes and stateful values.

The way the protobuf generated code is, one has to make a struct and then implement a trait (simple enough). I’m able to propagate pure values down into the generated gRPC services, but stateful values are proving more difficult as they obviously won’t Copy. Instead, I started by passing in references, which required lifetimes on the struct and impl:

Whilst this allowed usage of the value at the points needed, I encountered problems with the lifetime checker when trying to tie the knot at the top of the function chain:

As the function being called here is generated from the proto files, I cannot control the types or lifetimes being specified. Best I can tell at this point, the only option would be to use something like the lazy_const crate and treat the stateful value (which happens to be a network client with connection pool) and then treat said value as global? If there are alternate (or better) ways to of doing this kind of thing, without global state, that would be great to hear about.

Thanks for any advice in advance!

– T


#2

That 'static lifetime constraint just means that the type has to either own it’s values or contain references to something with a static lifetime. So you can have Service own both config and client, use Rc/Arc instead of normal references, or use lazy_static.


#3

I second the Rc/Arc suggestion. It sounds like this is creating a service which might run arbitrarily long compared to the rest of your code and will need to be able to ensure that the data you give it is kept alive for at least as long as it runs. This is basically the definition of 'static.

Some random tips:

I’m not sure your level of familiarity with lifetimes yet, but the signature of start is a bit of a red flag.

// This guy here
pub fn start<'a>(cfg: &'a Config, consul: &'a ConsulClient)

// is better written as
pub fn start(cfg: &Config, consul: &ConsulClient)

// which is actually equivalent to
pub fn start<'a,'b>(cfg: &'a Config, consul: &'b ConsulClient)

…which for all intents and purposes is basically indistinguishable from what you have, but eventually you will write a function with a signature where such a difference does matter, and that’s when virtually almost all of the time you will want the lifetimes to be different (whether you realize this yet or not).

…actually, let me reconsider this advice. Do deliberately annotate functions like this according to your current mental model, and then when it eventually breaks and the compiler complains about unsatisfiable lifetimes, you can start experimenting by deleting lifetime annotations. Then compare your old signatures to those produced by lifetime elision, and figure out a better mental model.

That’s how I learned, anyways. Can’t learn without making mistakes, I say.


Also it seems to me that some of your “cargo install --force” commands in the makefile might be better specified as build-dependencies in Cargo.toml.


#4

Ahh! Now you folks have pointed out "just use arc" that seems obvious… not sure why it didn’t occur to me before.

Thanks @ExpHP for the advice on lifetimes. Most of the time I find them pretty intuitive; thus far i’ve only encountered a couple of cases where the “right thing” was not immediately obvious but i’ll certainly take your advice.

And yep you’re right about the cargo definition and the makefile. I set that up when i was just getting started with Rust, left the project for a few months as Google was still breaking stuff upstream, and only just returned to it this past week so there’s certainly a bunch of things that need cleaning up.

Thanks so much folks, i’ll let you know how it goes with Arc!