I'm writing a tower::Service implementation for a Rust Lambda function utilizing the lambda_runtime crate and I'm having difficulty with the Future type on the service.
To make things much easier to reason about and work with, I have a struct which contains config data that I've defined a function on:
error: lifetime may not live long enough
--> crates/api-lambda/src/lib.rs:20:9
|
19 | fn call(&mut self, req: LambdaEvent<serde_json::Value>) -> Self::Future {
| - let's call the lifetime of this reference `'1`
20 | Box::pin(self.on_event(req))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'static`
I think I understand why the issue is manifesting, self needs to be 'static? Obviously I don't see any way of making self 'static, so what are my options to work around this?
the Future may runs concurrently (e.g. spawned as a task, or as part of a task), thus it requires 'static, which means you cannot borrowSelf (unless you use a global or thread local, which could work, but is highly discouraged), it must be some owned type, e.g. Arc.
you should refactor the code so the on_event() async function does not need to access the whole Self, and only pass the minimal states needed as arguments. typically, you should split the LambdaService type (i.e. Self) into sub components and use Arc for the part that is required to handle the request.
for instance, suppose the LambdaServiceConfig type is what's actually needed to serve the request, then you can use Arc for the config field:
when implementing Service, you simply make a clone of the Arc:
fn call(&mut self, req: LambdaEvent<Value>) -> Self::Future {
// Arc::clone() can be called using method syntax too, it's just a personal preference
Box::pin(Arc::clone(&self.config), req))
}
this principle also applies in the extreme case where the entire LambdaService is needed in the event handler. if that's the case, you should just rename the LambdaService to something like LambdaEventHandler, and make a new LambdaService type, which is just a wrapper of Arc<LambdaEventHandler>, and implement Service for that type instead:
struct LambdaEventHandler {
config: LambdaServiceConfig,
}
impl LambdaEventHandler {
// this takes `Arc<self>` so it can be spawned as a task
// in some cases, you may want to use `Weak` instead of `Arc` though
// but then you cannot use method call syntax, it must be an associated function
async fn on_event(self: Arc<Self>, req: LambdaEvent<Value) -> Result<Option<LambdaResponse>> {
todo!()
}
}
// we cannot impl Service for Arc<LambdaEventHandler> due to orphan rule
// use a newtype wrapper instead
struct LambdaService {
handler_state: Arc<LambdaEventHandler>
}
impl Service<LambdaEvent<Value>> for LambdaService {
//...
fn call(&mut self, req: LambdaEvent<serde_json::Value>) -> Self::Future {
let handler = Arc::clone(&self.handler_state);
Box::pin(handler.on_event(req))
}
}
@nerditation thank you for helping me make sense of it.
Is it safe/accurate to summarize that the impl Future returned by LambdaService::on_event(&self, ...) has a lifetime that is not the same as &self: LambdaService, and since the lifetime of the future is not tied to the lifetime of &self, the future may outlive self, and thus the compiler rejects it?
I was able to adjust my code to make it possible to keep using it the way I'm using it by redefining LambdaService:
Thus by creating an owned clone of &self LambdaService, moving that into the async closure, the lifetimes of the returned future and the cloned LambdaService are the same, so it works!
I know I can use your example of just abstracting out config and other fields, but I like the ergonomics of this better.
I would phrase like this: the impl Future captures the lifetime 'a of self: &'a Self, (thus, it is tied to &self). but the definition of the Service trait requires the return type of call() method to be not tied to the lifetime 'a of self: &'a mut Self)[1], and this requirement is enforced by the compiler as if the type has a 'static bound, then it can be checked using the normal borrow checker rules, illustratively:
Self::Future OUTLIVES 'static //<-- the requirement of `Service` trait
on_event() OUTLIVES Self::Future //<-- unsize coersion of `Box`
&'1 self OUTLIVES on_event() //<-- `on_event()` captures `&'1 self` covariantly
so transitively, you get the error message you saw:
returning this value requires that '1 must outlive 'static
however, your wording sounds backward to me, I'm not sure if it's just a phrasing difference, or it is because your intuition is not quite correct.
the solution is valid, but your explanation is not accurate. you moved a clone of LambdaService (which is a wrapper of Arc<LambdaServiceInner>) into the Future, not what you described as "an owned clone of &self LambdaService"
note, your LambdaServiceInner type serves the same purpose of the LambdaEventHandler type in my example, that is, allocate it on the heap and use an Arc of it so it can be captured by value (move into) in the Future instead of by reference (borrow).
the reason this works is because the async move {} block generate a different Future than the async fn on_event() return value: the former captures LambdaService by value, not by reference, so it is 'static.
let's desugar the synthetic Future types by hand, let's call the on_event() type OnEvent, and the async move {} type AsyncBlock (the following pseudo code is simplified for demonstration, it's NOT exactly how the compiler does it).
the async fn on_event() is desugared to something like this, note it has a lifetime 'a because it captures &self by reference:
on the other hand, the async move {} block generates something like this (it has an await point, so I use an enum to represent the state machine), note it doesn't have a lifetime in the type:
enum AsyncBlock {
/// before the `await`
State0 {
captured_self: LambdaService, //<-- by value, not by ref
captured_req: LambdaEvent<serde_json::Value>,
}
/// retry and after `await`
State1 {
/// compiler magic!!! self-referential type
/// note, this "pseudo" lifetime doesn't appear in `AsyncBlock` itself
on_event: OnEvent<'self_ref>,
/// this is not directly accessed after the `await` point,
/// but since `on_event` borrows it, it needs to be kept alive
/// it needs to be kept alive also because of the scope of drop,
/// same for `captured_req`, but omitted for brevity
captured_self: LambdaService,
}
}
impl Future for AsyncBlock {
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
match self {
State0 { captured_self, captured_req } => {
let on_event = captured_self.on_event(captured_req);
// !!! this is NOT how the self referential data is actually constructed
*self = Self::State1 { on_event, captured_self };
self.poll(cx)
}
State1 { ref on_event, .. } => {
on_event.poll(cx)
}
}
}
}
the returned Future is of the Self::Future associated type, which is a single type, not a GAT↩︎
I think your practical concerns have been addressed. I'm writing this comment primarily to (hopefully) help your understanding from a type-system perspective.
If on_event doesn't need to capture &self, another option is:
impl LambdaService {
/*
// The original method had this desugared signature
pub fn on_event<'s>(&'s self, event: LambdaEvent<serde_json::Value>)
->
impl Future<Output = anyhow::Result<Option<LambdaResponse>>> + use<'s>
*/
pub fn on_event(&self, event: LambdaEvent<serde_json::Value>)
-> // Here's the difference: vv
impl Future<Output = anyhow::Result<Option<LambdaResponse>>> + use<>
{
Ok(None)
}
}
Then the borrow of *self won't be captured and the future will satisfy a 'static bound.
The key point that's easy to miss is: async fn futures capture all of their input generics by default. That means they keep all their inputs borrows alive by default.
You may be interpreting "lifetimes" to be directly about the liveness of values. Despite the unfortunate overlap in naming, Rust lifetimes generally reflect the duration of borrows. The compiler doesn't directly reason in a manner like "the future might outlive the LambdaService".
In your OP, the impl Future returned by on_event captures the lifetime from &self. This means that the receiver will remain borrowed so long as the future is in use, and that -- at a type level[1] -- the future can only meet a 'static bound if you passed in a &'static self.
You can only coerce T to dyn Trait + 't if T: 't. In your case, that would mean the future would have to meet a 'static bound. But as just mentioned, that would require a &'static self. So that is what the compiler error was trying to convey with the original error ("requires that '1 must outlive 'static").
If you don't actually need to capture that borrow, you can commit to never doing so by using precise capturing (+ use<> in the code example above). If you do, or want to keep the option open, precise capturing won't help.
@nerditation explained how your solution works, so I won't duplicate that effort.
In terms of the Service trait itself...
pub trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
fn call(&mut self, req: Request) -> Self::Future;
// ...
}
The only way for a lifetime (besides 'static) to appear in any of the associated types Response, Error, or Future is if the lifetimes were part of Request or part of the implementing type. Rust won't allow anything else. In particular, there's no way to make the lifetime from &mut self on call appear in these associated types. Given the same Self and Request types, those three associated types must be the same for every invocation of call, no matter the lifetime on &mut self.
Corollary: if Self: 'static and Request: 'static, then Future: 'static.[2] Because there's no way for a non-'static lifetime to appear in the associated types. (This is the case with your examples.)
So when, from a practical perspective, the implementing type and any trait parameters need to meet a 'static bound, you can also consider plain[3] associated types to be 'static. And to reiterate: even when they are not 'static, plain associated types can't capture the outer &self and &mut self lifetimes.[4]
Probably everything you're doing with Tower is such a case, where everything needs to meet a 'static bound to be practical. So I'm pretty sure it would be useless for you to do this next part.
But as an exercise, let's see how a lifetime could get into one of those associated types:
impl LambdaService {
// Like the OP, the future is `+ use<'_>` -- captures the borrow from `&self`
pub async fn on_event(&self, event: LambdaEvent<serde_json::Value>)
->
anyhow::Result<Option<LambdaResponse>>
{
Ok(None)
}
}
impl<'this> Service<LambdaEvent<serde_json::Value>> for &'this LambdaService {
type Response = Option<LambdaResponse>;
type Error = anyhow::Error;
type Future = Pin<Box<
// from the implementing type: vvvvv
dyn Future<Output=Result<Self::Response, Self::Error>> + 'this
>>;
fn call(&mut self, req: LambdaEvent<serde_json::Value>) -> Self::Future {
// This effectively copies the `&'this LambdaService` (`*self`),
// calls the method on it to get an `impl Future<..> + use<'this>`,
// boxes and pins that, and finally coerces to `dyn .. + 'this`
Box::pin(self.on_event(req))
}
// ...
}
to both @nerditation and @quinedot thank you so much for your responses, I'm going to read this a few dozen times and try things out to make sure I understand this. Your answers are very helpful, if after all this I'm unable to brain it, I'll respond, but kudos for taking the time to explain.
@quinedot so I tried out the last example you provided with lifetimes and my code compiles even when the Arc is removed. You mention it being possibly "useless," but can you explain why it's useless? Should I still be using an Arc internally, or is this method preferred?
I assumed whatever async service you're building would require 'static tasks at some point (as that is how most async runtimes operate), and that such a requirement wouldn't be compatible with my last example. My assumption may have been wrong. Or, if your project isn't complete, lifetime problems might pop up later instead. I really don't know which.
because it is safe to leak values in rust, it is impossible to soundly implement structured concurrency in style of the scoped thread API for async tasks. this makes non-'static futures very inconvenient to use, besides block_on() it, you can only achieve concurrency in two ways:
future combinators, e.g. the futures crate's join(), select(), etc
this is multiplexed (i.e. single threaded) concurrency, but no parallelism
spawn on explicitly managed scheduler/executor
this requires the entire application to be rewritten to make use of it
one implementation of this kind of API is async-executor
as far as I know, tokio doesn't provide such API natively
I believe async-executor is compatible with tokio, but it's not very easy to integrate, and the performance is expected to be worse than tokio's internal scheduler.
in a typical application, you'll most likely need a 'static bound at some point, sooner or later.
as for Arc, it should not be a concern in terms of performance and/or memory overhead, unless you use it imporperly (e.g. cloning and dropping Arc in hot loops for high contention resources).