openfaas / store

Official Function and Template Store for OpenFaaS
MIT License
162 stars 56 forks source link

Submission: Rust actix-web template #141

Open baremaximum opened 3 years ago

baremaximum commented 3 years ago

Hi,

I would like to submit a proposal for an openfaas template that uses the actix-web framework in Rust. Here is a link to the repo containing the template: https://github.com/baremaximum/rust-actix-web-template

Why I made this template

While there are some Rust templates, none of them take full advantage of the strengths of the HTTP watchdog.

The HTTP template that does exist (https://github.com/openfaas-incubator/rust-http-template) doesn't work. Even if it did, it uses the low-level 'hyper' crate. This crate provides a correct HTTP implementation that is meant to serve as the basis for building web frameworks or clients. As a result, it is lacking many of the features that you would find in a fully fleshed out framework. For example, it doesn't implement any kind of multi-threading. It also doesn't provide any means to persist objects between requests/threads. Any user that wants to use multi-threading, or that wants to maintain a persistent database connection pool, would be discouraged to learn that they have to write that functionality themselves. Unfortunately, these features are some of main reasons to use of-watchdog templates. I don't think it makes sense for a template to make it that difficult use the watchdog's advantages.

I made this template to provide an alternative that doesn't have these limitations.

Why actix-web (pros)?

Actix-web is a popular, high performance, fully featured web framework written in Rust. It's about as fast as it gets (see benchmarks ). Compared to NodeJS, an actix-web server can serve ~7 times as many requests per second under heavy load, at a small fraction of the cost (see this article ). It also has a near instantaneous ( <1ms) start-up time, and a tiny binary size. The image built from this template is less than 40 mb,, which is less than 20 mb more than the base alpine + of-watchdog image. As a result, actix-web is perfectly suited for situations where rapid and efficient scaling is required.

All of these factors combine to make actix-web a very attractive candidate for writing HTTP functions.

Why not actix-web (cons)?

Rust is less popular than most of the other languages used by openfaas templates.. As a result, the community is smaller, and there aren't as many "battle-tested" libraries.

Using an HTTP framework means that users will have to modify files in the template most of the time, instead of just modifying the handler. This results in an awkward workflow and potentially bloated directory structure. Unfortunately, it's inherently impossible to make a single application that meets everyone's needs. Middleware, and plugin requirements for example are likely to change on different projects, and these are never set at the handler level. However, this problem isn't unique to this template.

Finally...

If you read all of that, thanks!

LucasRoesler commented 3 years ago

@baremaximum you mention this

Using an HTTP framework means that users will have to modify files in the template most of the time, instead of just modifying the handler.

which worries me a little bit. Can you give some concrete examples of when a developer might want to do this? Perhaps there is a reasonable way to address the most common cases by exposing "hooks", for example, there could be a (potentially empty) list of middlewares defined in the function handler implementation and the main could import and apply them to the server? The example you give in your template README feels like it isn't a slight modification of the template, it is a full fork and refactor, the call signature of the handler isn't even the same

pub async fn handler(
    bean_count: web::Data<AtomicI32>, // application passes state to handler
    item: web::Json<BeanChange>, // deserialized json from request
) -> impl Responder

So I guess my question is: do users regularly need to hack the main?

baremaximum commented 3 years ago

Thanks for taking the time to look at this!

Users certainly wouldn't have to modify the main every time. The template is totally usable out of the box, and has reasonable minimal defaults set. It's maybe slightly more likely to require modification at the template level than the Node template because the Node template defines a route for each HTTP method, whereas this one does not. I didn't include multiple methods because that would involve adding unused routes and unused code by default. Personally I prefer making templates that only include what I know I will need, and that I can add to if it's needed instead of making templates that I will have to remove things from every time.

In regards to having to modify the entrypoint in the template, most - if not all - HTTP framework based templates are going to run in to this same issue. It's rare that frameworks allow extensive app configuration from the handler level (I've never seen it), and a lot of use cases require at least some configuration.

Take for example someone that wants to use the cookieparser library in a function that uses the Node template (a likely scenario). They also would have to go into the template and add cookieparser to the app object in there. It's the same thing. This template isn't any more likely to require template modification than any of the other HTTP framework templates. I just talked about it more in the documentation.

As for the call signature of the handler changing, it's not really changing. Handler call signatures are a tuple that implements the FromRequest trait, each of whose members also implements the FromRequest trait. Here is a link to where that implementation is defined:

https://github.com/actix/actix-web/blob/master/src/extract.rs

So, in a handler call, you can pass a list of up to 9 arguments. As long as the type of each argument implements the FromRequest trait, the handler will compile, and the value from the extractor defined in the called signature will be available.

This design makes it very easy to extract data from requests. You can just put the appropriate extractor in your handlers call, instead of having to extract it from a req.body object.

The use of duck typing to allow for flexibility in call signatures like this isn't very unusual. For example, Express handlers also have a variable call signature for handlers. The args array is looped over, and the request is passed along the chain sequentially. This simple example demonstrates that:

import express from 'express';

const app = express();

const testOne = (req, res, next) => {
    console.log("hello from one");
    next();
}
const testTwo = (req, res, next) => {
    console.log("hello from two");
    next();
}

app.get('/one', testOne, (req, res) => {
    console.log("Test One");
    res.send(200);

})

app.get('/two', testOne, testTwo, (req, res) => {
    console.log("Test Two")
    res.send(200);
})

app.listen(3000, console.log('started server'))

If you send a request to the '/two' route, you'll see the middleware functions get called in the order in which they are passed. As long as a function takes a 'next' callback, it can be added to this chain.

So I don't think that having duck typed call signatures is a problem.

alexellis commented 3 years ago

I'm chatting with Lucas on our weekly call and reviewing the example.

The place we're not sure is where you mention editing the main / entrypoint which is not supported as a use-case for the template store.

use actix_web::{post, HttpRequest, Responder};

#[post("/")]
pub async fn handler(_req: HttpRequest) -> impl Responder {
    "OK" // Do whatever you want here
}

If you can have a generic template which works similar to our Golang middleware template, then we think it would make sense to accept it.

Enhancements / proposals for the rust or rust-http templates would also be appreciated.

baremaximum commented 3 years ago

I think this could be possible without making too many changes to the template. However, it may lead to users having to write their own implementation of some of the framework's features. I'll take a closer look at this sometime in the next couple days.

alexellis commented 3 years ago

Thanks

baremaximum commented 3 years ago

@alexellis I made some changes to the template so that users would no longer have to modify the template for anything. I also modified the README to reflect those changes.

I ended up just moving the app object out of the main.rs in the template, and into a factory function in the handler function crate. That way users can make whatever modifications they want to the app object from there.

While this solution feels a bit strange, there isn't really any other way to implement a lot of features. Persistent objects need to be attached to the app factory, which holds persistent data in an array. The http server creates a new app instance for each thread using the app factory. The only way to modify the app factory is through the app's methods since all its fields are private. So there is no way to avoid allowing users to modify the app factory.

I tried making a smaller factory function that only returns the app object instead of an HttpServer with all the env variable stuff, but this ended up not being possible because the app factory's type depends on private type definitions in actix.

If the file is too big, I could maybe move the app factory to a separate file in the function library crate.