Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implements Promise for dispatch callback #1377

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

jizhuozhi
Copy link
Collaborator

@jizhuozhi jizhuozhi commented Oct 10, 2024

Ⅰ. Describe what this PR did

In the current envoy WASM plugins, if we need IO request such as HTTP/GRPC/Redis, we must register request to envoy event loop and assigned a token via such as dispatch_http_call and WASM plugin should yield current request lifetime using Action::Pause. When the IO request completed, the envoy will callback to WASM plugin via on_http_call_response, and plugin should dispatch response using token (or ignored if single IO request). If we want to share something between dispatch_http_call and on_http_call_response, we must share them in plugin context fields, it's not a suitable scope.

Here is an example from proxy-wasm-rust-sdk

impl HttpContext for HttpAuthRandom {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        self.dispatch_http_call(
            "httpbin",
            vec![
                (":method", "GET"),
                (":path", "/bytes/1"),
                (":authority", "httpbin.org"),
            ],
            None,
            vec![],
            Duration::from_secs(1),
        )
        .unwrap();
        Action::Pause
    }

    fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
        self.set_http_response_header("Powered-By", Some("proxy-wasm"));
        Action::Continue
    }
}

impl Context for HttpAuthRandom {
    fn on_http_call_response(&mut self, _: u32, _: usize, body_size: usize, _: usize) {
        if let Some(body) = self.get_http_call_response_body(0, body_size) {
            if !body.is_empty() && body[0] % 2 == 0 {
                info!("Access granted.");
                self.resume_http_request();
                return;
            }
        }
        info!("Access forbidden.");
        self.send_http_response(
            403,
            vec![("Powered-By", "proxy-wasm")],
            Some(b"Access forbidden.\n"),
        );
    }
}

So there is three major problem we need to resolve:

  1. How to easier dispatch request and callback response via token.
  2. How to share something between dispatch request and callback response with smaller scope.
  3. How to make code more fluid instead of spreading logic in different places

In Rust async programming, normally we use async/await for IO request, but in envoy WASM plugin, there is no executor to poll future. A suitable solution is providing JavaScript style Promise (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). With Promise, we could write all logic in single function

impl HttpContext for HttpAuthRandom {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        let token = self.dispatch_http_call(
            "httpbin",
            vec![
                (":method", "GET"),
                (":path", "/bytes/1"),
                (":authority", "httpbin.org"),
            ],
            None,
            vec![],
            Duration::from_secs(1),
        )
        .unwrap();
        let promise = Promise::new();
        // make relation between promise and request (token)
        promise.then(|(_, _, body_size, _)| {
            if let Some(body) = hostcalls::get_http_call_response_body(0, body_size) {
                if !body.is_empty() && body[0] % 2 == 0 {
                    info!("Access granted.");
                    hostcalls::resume_http_request();
                    return;
                }
            }
            info!("Access forbidden.");
            hostcalls::send_http_response(
                403,
                vec![("Powered-By", "proxy-wasm")],
                Some(b"Access forbidden.\n"),
            );
        })
        Action::Pause
    }

    fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
        self.set_http_response_header("Powered-By", Some("proxy-wasm"));
        Action::Continue
    }
}

It seems more fluid then writing in callback of on_http_call_response, but there is no executor for promise to trigger state transferring. We can use on_http_call_response as trigger simply

struct HttpAuthRandom {
    promise: Rc<Promise<(u32, usize, usize, usize)>>
}

impl HttpContext for HttpAuthRandom {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
      // ...
      // make relation between promise and request (token)
      self.promise = promise.clone();
      // ...
    }
}

impl Context for HttpAuthRandom {
    fn on_http_call_response(&mut self, _token_id: u32, _num_headers: usize, _body_size: usize, _num_trailers: usize) {
        self.promise.fulfill((_token_id, _num_headers, _body_size, _num_trailers))
    }
}

As for making relationship between multi tokens and promises, we could just using HashMap with insert/remove (maybe it should be embed in SDK but not belongs to this PR)

struct HttpAuthRandom {
    m: HashMap<u32, Rc<Promise<(u32, usize, usize, usize)>>
}

impl HttpContext for HttpAuthRandom {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
      // ...
      // make relation between promise and request (token)
      self.m.insert(token, promise.clone());
      // ...
    }
}

impl Context for HttpAuthRandom {
    fn on_http_call_response(&mut self, _token_id: u32, _num_headers: usize, _body_size: usize, _num_trailers: usize) {
        let promise = self.m.remove(token);
        promise.fulfill((_token_id, _num_headers, _body_size, _num_trailers))
    }
}

Ⅱ. Does this pull request fix one issue?

Ⅲ. Why don't you add test cases (unit test/integration test)?

Ⅳ. Describe how to verify it

Ⅴ. Special notes for reviews

@codecov-commenter
Copy link

codecov-commenter commented Oct 10, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 43.93%. Comparing base (ef31e09) to head (b61d6d3).
Report is 137 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1377      +/-   ##
==========================================
+ Coverage   35.91%   43.93%   +8.02%     
==========================================
  Files          69       76       +7     
  Lines       11576     9965    -1611     
==========================================
+ Hits         4157     4378     +221     
+ Misses       7104     5253    -1851     
- Partials      315      334      +19     

see 91 files with indirect coverage changes

Copy link
Collaborator

@johnlanni johnlanni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's integrate the functionality in this PR's plugin wrapper further. Hopefully, we can complete the httpcall feature for rust in this PR. For users, it should be very simple to use

Comment on lines +80 to +90
fn on_http_call_response(
&mut self,
_token_id: u32,
_num_headers: usize,
_body_size: usize,
_num_trailers: usize,
) {
self.http_dispatcher
.callback(_token_id, _num_headers, _body_size, _num_trailers)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic should be placed on the SDK side, so the user's code doesn't need to worry about this detail

Comment on lines +27 to +34
pub fn dispatch(
&mut self,
upstream: &str,
headers: Vec<(&str, &str)>,
body: Option<&[u8]>,
trailers: Vec<(&str, &str)>,
timeout: Duration,
) -> Rc<Promise<(u32, usize, usize, usize)>> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on this, further encapsulation is needed, such as supporting the extraction of domain and path from URL and cluster, as well as supporting get/post, etc

@johnlanni
Copy link
Collaborator

cc @007gzs

if let Some(body) = hostcalls::get_http_call_response_body(0, _body_size) {
if !body.is_empty() && body[0] % 2 == 0 {
log.info("Access granted.");
hostcalls::resume_http_request();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必须直接调用hostcalls对普通用户是不是不太友好

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants