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

implement map_response for deferred responses in rhai #1501

Merged
merged 10 commits into from
Aug 22, 2022

Conversation

Geal
Copy link
Contributor

@Geal Geal commented Aug 12, 2022

Fix #1219
Fix #1409

we need a specific function to map over deferred responses, so we can
make the distinction with the first one, where we have access to headers

we need a specific function to map over deferred responses, so we can
make the distinction with the first one, where we have access to headers
@github-actions

This comment has been minimized.

@Geal Geal force-pushed the geal/map_deferred_response branch from 2e38ba3 to 9897190 Compare August 16, 2022 09:20
@Geal Geal marked this pull request as ready for review August 16, 2022 09:22
@Geal Geal requested review from bnjjj and garypen August 16, 2022 10:01
@Geal Geal self-assigned this Aug 16, 2022
Copy link
Contributor

@garypen garypen left a comment

Choose a reason for hiding this comment

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

This looks great and it's nice to see the rhai binding catching up with the defer enhancements.

I have some questions about how to use this. It seems like an implementation of RouterService or ExecutionService has to decide to either map_response() or map_deferred_response(). I think it would be nice to actually "hide" this decision from rhai scripters if possible, so that map_deferred_response is implicitly chosen for Router and Execution services that call map_response and not possible to be chosen for QueryPlanning and Subgraph Services.

(I'm aware we can't force this kind of decision on rust plugin users, so we have to expose the complexity there, but it would be really nice to hide this from rhai scripters.)

Once this is resolved, we need to think about how best to document this new capability in the router doc pages for rhai.

@Geal
Copy link
Contributor Author

Geal commented Aug 18, 2022

it would be possible to do everything in the same response, but then we need to address the issue of header edition: headers can only be modified when looking at the first response, not deferred ones (otherwise we would have to buffer the entire http response).

Another thing I worry about here is how to link deferred responses. It looks like map_response and map_deferred_response are called independently, so there's no way to know in which serie of responses it is executing, is that correct? I guess that's the same for map_request vs map_response as well, so if we want to keep common info between responses we have to use the context?

Ideally I'd like an API like this:

fn router_service(service) {                                                
    let f = |response| {                                        
        let start = apollo_start.elapsed;                                                   
        let duration = apollo_start.elapsed - start;            
        print(`response processing took: ${duration}`);                          
        print(response.body.errors); 

         let g = |response| {                                        
                let start = apollo_start.elapsed;                       
                let duration = apollo_start.elapsed - start;            
                print(`deferred response processing took: ${duration}`);               
                print(response.body.errors);                            
         }; 
         response.map_deferred(g);                                      
    };                                                                             
    service.map_response(f);                                                            

But I am not sure it is achievable

@Geal Geal changed the title implement map_deferred_response in rhai implement map_response for deferred responses in rhai Aug 18, 2022
@Geal
Copy link
Contributor Author

Geal commented Aug 18, 2022

ok, now it is a bit nicer with the same method used everywhere. Accessing the headers from a deferred response will throw:

fn supergraph_service(service) {                                 
    let f = |response| {                                         
        let start = apollo_start.elapsed;                        
        let duration = apollo_start.elapsed - start;             
        print(`response processing took: ${duration}`);          
        try {                                                    
          print(`headers: ${response.headers}`);                 
        } catch(err) {                                           
          print(`cannot access headers from a deferred response`)
        }                                                        
                                                                 
        print(response.body.errors);                             
    };                                                           
    service.map_response(f);                                     
}                                                                

should I add a is_deferred method to the response object?

@garypen
Copy link
Contributor

garypen commented Aug 18, 2022

Talked about this with @SimonSapin during our API:1.0 review and he raised the point that it will be difficult for a programmer to deal with runtime errors due to handling of deferred responses. It's difficult to know what the "best" solution is here, because it's clearly a compromise whatever we do. However, your is_deferred() suggestion is definitely pointing in another direction. Rather than making rhai scripters understand about defer/not defer, would it be better to have a function which does something like: headers_are_available()? So, the example would become:

fn supergraph_service(service) {                                 
    let f = |response| {                                         
        let start = apollo_start.elapsed;                        
        let duration = apollo_start.elapsed - start;             
        print(`response processing took: ${duration}`); 
        if response.headers_are_available() {
          print(`headers: ${response.headers}`);                 
        } else {
          print(`cannot access headers from a deferred response`)
        }                                                                 
        print(response.body.errors);                             
    };                                                           
    service.map_response(f);                                     
}                                                                

I think it's more idiomatic rhai to do what your example does and catch the exception rather than have the headers_are_available() method, but I'm bringing it up for discussion since I know Simon didn't like the fact that dealing with runtime errors was a surprise and a bit of a pain.

I think with documentation and judicious use of try...catch, the best outcome is available, since even if we added headers_are_available() we'd still have to throw an error if they weren't...

@Geal
Copy link
Contributor Author

Geal commented Aug 18, 2022

ok so the headers_are_available method is there, but maybe the name should be more explicit about its usage? Somethings like response_headers_available because the headers would obviously be available in the request?

@garypen
Copy link
Contributor

garypen commented Aug 18, 2022

ok so the headers_are_available method is there, but maybe the name should be more explicit about its usage? Somethings like response_headers_available because the headers would obviously be available in the request?

I think the name is clear enough already since it's a method on a Response, so you already know you are dealing with a response.

I still think it would be better to not have this function and just handle the exception like you did in your original example. @SimonSapin could you look at the examples and offer up an opinion?

BTW: I imagine for your example that won't work right now since we haven't added support for accessing headers on a DeferredResponse. We'll need something like:

    #[rhai_fn(get = "headers", pure, return_raw)]
    pub(crate) fn get_originating_headers_execution_deferred_response(
        _obj: &mut SharedMut<execution::DeferredResponse>,
    ) -> Result<HeaderMap, Box<EvalAltResult>> {
        Err("cannot access headers on a deferred response.".into())
    }

and the same again for router::DeferredResponse. And the same pairing for setting of headers...

@Geal
Copy link
Contributor Author

Geal commented Aug 18, 2022

The example works because it throws due to a missing function instead of an explicit error

Geoffroy Couprie added 2 commits August 19, 2022 15:17
@Geal Geal enabled auto-merge (squash) August 22, 2022 09:18
@Geal Geal merged commit da43120 into main Aug 22, 2022
@Geal Geal deleted the geal/map_deferred_response branch August 22, 2022 09:39
@Geal Geal mentioned this pull request Aug 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants