Skip to content

how to create custom context #110

Closed
Closed

Description

@infinite-Joy

I have an expensive task that should not be part of the handler. As that would be running the code again and again for each event call. According to AWS documentation the execution context is the place where these "global" code needs to go so that the output of the code is persistent over events. The docs say that "A new instance of the Context object is passed to each handler invocation". Is there a way create a custom context which saves the global state of the object between calls?

Activity

  1. softprops commented on Jun 12, 2019

    @softprops
    Contributor

    expensive task that should not be part of the handler

    A bit of background on Lambda contexts. Each invocation of a Lambda function is provided a new context value which captures a set of introspective information about the function and optionally calling context information thats consistent across all types of event trigger. You can find documentation of how the Rust runtime represents that here. It's typically not a place to store user data because you'll get a new copy on each invocation as a function argument with new information.

    Globals in Lambda are often used specifically as an optimization for cached initialization.

    An example may be a function that requires a tokio Runtime in function code. Rather than recreate a new Runtime on every Lambda invocation which can be expensive, you might store a single Runtime in a thread local and share via RefCell to reduce the cost of initializing a new one on each . Here's a rough sketch for what that might look like.

    thread_local!(static RUNTIME: RefCell<Runtime> = {
        RefCell::new(Runtime::new().unwrap())
    });
    
    fn main() {
        lambda!(handler);
    }
    
    fn handler(
        event: Value,
        ctx: Context,
    ) -> Result<(), HandlerError> {
        RUNTIME.with(|rt| {
            rt.borrow_mut()
                .block_on(future_work(event))?;
            Ok(())
        })
    } 

    https://docs.rs/lazy_static/1.3.0/lazy_static/ is another crate that you may find helpful.

    Another option which I haven't myself explored but I think would be interesting is to implement the Handler trait for a custom struct that captures what you may store in global state within its struct fields. Typically you'll be using closures or fn defs to implement your lambda which already have implementations for Handler trait but that doesn't stop you from implementing Handler for your own struct.

    Below is a rough sketch for what that might look like

    struct Func {
      data: usize
    }
    
    impl Handler<Value, usize, HandlerError> for Func {
      fn run(&mut self, event: Value, ctx: Context) -> Result<usize, HandlerError> {
          self.data += 1;
      }
    }
    
    fn main() {
        lambda!(Func { data: 0 });
    }
  2. arahalevich-move commented on Jun 14, 2019

    @arahalevich-move

    Thank you @softprops for the overview, last option looks pretty neat!

  3. softprops commented on Jun 14, 2019

    @softprops
    Contributor

    I haven't tried it myself you so I'd be curious to hear how that goes for you

  4. stacygrace commented on Mar 22, 2020

    @stacygrace

    I gave the last option a try and its working pretty great for me.

    Don't have any code that I can share just yet but I'll try to put up a demo repo to link to soon. Thank you @softprops for that idea!

  5. huntc commented on Dec 2, 2020

    @huntc

    I was wondering if an example for the current Handler interface could be provided in the context of http. I'm having a tricky time understanding how it should be declared. Thanks for any help. Here's the type I'm referring to:

    /// Functions serving as ALB and API Gateway REST and HTTP API handlers must conform to this type.
    ///
    /// This can be viewed as a `lambda::Handler` constrained to `http` crate `Request` and `Response` types
    pub trait Handler: Sized {
        /// The type of Error that this Handler will return
        type Error;
        /// The type of Response this Handler will return
        type Response: IntoResponse;
        /// The type of Future this Handler will return
        type Fut: Future<Output = Result<Self::Response, Self::Error>> + 'static;
        /// Function used to execute handler behavior
        fn call(&mut self, event: Request, context: Context) -> Self::Fut;
    }

    My attempt so far:

    impl Handler for MyStructWithContext {
        type Error = Error;
        type Response = Response<Body>;
        type Fut = dyn Future<Output = Result<Self::Response, Self::Error>> + 'static;
    
        fn call(&mut self, event: Request, context: Context) -> Self::Fut {
            unimplemented!()
        }
    }

    ...results in:

    19  |     type Fut = dyn Future<Output = Result<Self::Response, Self::Error>> + 'static;
        |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
    

    ...and I'm trying to fathom exactly what of those types doesn't have a known size.

  6. softprops commented on Dec 3, 2020

    @softprops
    Contributor

    I'm not sure if this is exactly what you mean but here's an example of an impl for a Fn type

    I think dyn objects themselves don't have a knowable size at compile time unless you wrap them in a Box

  7. huntc commented on Dec 5, 2020

    @huntc

    I'm not sure if this is exactly what you mean but here's an example of an impl for a Fn type

    I think dyn objects themselves don't have a knowable size at compile time unless you wrap them in a Box

    Thanks for the response. That code sample you provided is what I based my code on. Thanks for reminding about the use of box here too.

    I think it’s clear that AWS lambda needs to at least provide a hook, or a great example of code initialisation for Rust.

    Even better, folding over state with a scan-style function would be great, but perhaps that’s another issue. :-)

  8. leo-suspensify commented on May 2, 2021

    @leo-suspensify

    The library has changed a little since this question was first asked, but it does work with BoxFuture as follows:

    use futures::future::BoxFuture;
    use lambda_http::{handler, Body, Handler, Request, Response};
    use lambda_runtime::{Context, Error};
    
    struct Server {
        some_global_setting: String,
    }
    
    impl Handler for Server {
        type Error = Error;
        type Response = Response<Body>;
        type Fut = BoxFuture<'static, Result<Response<Body>, Error>>;
        fn call(
            &self,
            request: Request,
            context: Context,
        ) -> BoxFuture<'static, Result<Response<Body>, Error>> {
            //some_other_async_function(self.global_settings.clone(), request, context).boxed()
        }
    }
    
    #[tokio::main]
    async fn main() -> Result<(), Error> {
        let server = Server {
          some_global_setting: "blah".to_string();
        };
        let wrapped_handler = handler(server);
        lambda_runtime::run(wrapped_handler).await?;
        Ok(())
    }

    If this is the recommended solution then I can add it to the README

  9. Jacco commented on Dec 15, 2021

    @Jacco

    Am trying to do it a bit differently, and am struggling with lifetime management:

    pub fn my_handler_fn<F>(f: F, c: Client) -> MyHandlerFn<F> {
        MyHandlerFn { f, c }
    }
    
    #[derive(Clone, Debug)]
    pub struct MyHandlerFn<F> {
        f: F,
        c: Client
    }
    
    impl<F, A, B, Error, Fut> Handler<A, B> for MyHandlerFn<F>
    where
        F: Fn(A, lambda_runtime::Context, &Client) -> Fut,
        Fut: Future<Output = Result<B, Error>>,
        Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>> + std::fmt::Display,
    {
        type Error = Error;
        type Fut = Fut;
        fn call(&self, req: A, ctx: lambda_runtime::Context) -> Self::Fut {
            (self.f)(req, ctx, &self.c)
        }
    }

    And then the following:

    #[tokio::main]
    async fn main() -> Result<(), Error> {
        let shared_config = aws_config::load_from_env().await;
        let client = Client::new(&shared_config);
    
        let func = my_handler_fn(my_handler, client);
        lambda_runtime::run(func).await?;
        Ok(())
    }

    But I get life mismatch and one type is more general than the other errors on the line where run is called.

    Anybody know how to resolve this?

  10. nmoutschen commented on Dec 17, 2021

    @nmoutschen
    Contributor

    Hey @Jacco !

    Could you paste the entire error message?

    Also, is there any specific reason why you are creating a separate handler function rather than just using the call function in your MyHandlerFn struct?

    By the way, I've just added this example using a struct that implements the Handler trait for reference.

  11. locked and limited conversation to collaborators on Jan 19, 2022
  12. converted this issue into a discussion #405 on Jan 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions