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

Support for gRPC protocol #1623

Merged
merged 63 commits into from
Oct 21, 2020
Merged

Support for gRPC protocol #1623

merged 63 commits into from
Oct 21, 2020

Conversation

rogchap
Copy link
Contributor

@rogchap rogchap commented Sep 9, 2020

fixes #441

Initial implementation only supports Unary requests.

Example JavaScript API:

import grpc from 'k6/protocols/grpc';
import { check } from "k6";

const client = grpc.newClient();
client.load([], "samples/grpc_server/route_guide.proto")


export default () => {
    client.connect("localhost:10000", { plaintext: true })

    const response = client.invoke("main.RouteGuide/GetFeature", {
        latitude: 410248224,
        longitude: -747127767
    })

    check(response, { "status is OK": (r) => r && r.status === grpc.StatusOK });

    client.close()
}

@CLAassistant
Copy link

CLAassistant commented Sep 9, 2020

CLA assistant check
All committers have signed the CLA.

@rogchap rogchap changed the title [WIP] Support for gRPC protocol Support for gRPC protocol Sep 11, 2020
@codecov-commenter

This comment has been minimized.

@na-- na-- requested review from imiric and mstoykov September 11, 2020 06:09
@na-- na-- added this to the v0.29.0 milestone Sep 11, 2020
@rogchap rogchap requested a review from na-- October 19, 2020 00:59
@rogchap
Copy link
Contributor Author

rogchap commented Oct 19, 2020

Hey @na-- Hope you had a good weekend?
Ok...so I think I have all the changes done; and all CI checks are now green 😅
Let me know if I've missed anything, but I think we are good to merge now.

mstoykov
mstoykov previously approved these changes Oct 20, 2020
Copy link
Contributor

@mstoykov mstoykov left a comment

Choose a reason for hiding this comment

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

I can't find any major problems. I commented in two places on things that I think will be better but we can fix them later as well

timeout := 60 * time.Second

ctx = metadata.NewOutgoingContext(ctx, metadata.New(nil))
for k, v := range params {
Copy link
Contributor

Choose a reason for hiding this comment

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

I am more and more of the opinion that https://github.com/loadimpact/k6/blob/master/js/modules/k6/http/request.go#L248-L351 should be split and moved to utility functions and then reused here and in in the ws code.
But this should be done in a different PR

Copy link
Member

Choose a reason for hiding this comment

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

definitely a separate PR

Comment on lines +398 to +404

var response Response
response.Headers = header
response.Trailers = trailer

marshaler := protojson.MarshalOptions{EmitUnpopulated: true}

Copy link
Contributor

Choose a reason for hiding this comment

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

I do think it will be more readable if this is moved before the Invoke so taht if err != nil can be directly after the Invoke.
Also making the ctx should probably be directly before the Invoke and you can skip the header/trailer varaible and directly use response.Headers/response.Trailers as far as I can see

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I directly use response.Headers/response.Trailers this results in issues with casting, as one is map[string][]string and the other is metadata.MD which is an alias to map[string][]string. If I change the Response struct to use metadata.MD then this will not serialize correctly via goja (unmarshals to an empty object {})

close(errc)
}()

if err := <-errc; err != nil {
Copy link
Contributor

@mstoykov mstoykov Oct 20, 2020

Choose a reason for hiding this comment

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

Would canceling the ctx ... make the goroutine Dial return faster?
Maybe we can drop the goroutine ... have the transportCreds remember the first error they get and cancel the context on it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Possibly. We wound have to attach a context cancel function to the transportCreds object at the beginning, and in the event of an error call cancel(), and then store the TLS error on the transportCreds to later retrieve the error to check if the error was caused by TLS (as appose to some other context cancellation).

In my opinion this last part of checking an error field on a struct is not idiomatic Go and is prone to being missed (hence why I don't like the builder pattern in Go, you have to remember to check that error field).
At this point I'm still in-favour of the error channel an goroutine as I think it reads cleaner.


// MethodInfo holds information on any parsed method descriptors that can be used by the goja VM
type MethodInfo struct {
grpc.MethodInfo
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
grpc.MethodInfo
grpc.MethodInfo `json:"-" js:"-"`

I think I was a bit overzealous with the suggestion for code reuse here 😞 Without this change, if you run something like:

let methods = client.load([], "samples/grpc_server/route_guide.proto");
console.log(JSON.stringify(methods, null, 4));

you'd get this:

[
    {
        "method_info": {
            "name": "GetFeature",
            "is_client_stream": false,
            "is_server_stream": false
        },
        "name": "GetFeature",
        "is_client_stream": false,
        "is_server_stream": false,
        "package": "main",
        "service": "RouteGuide",
        "full_method": "/main.RouteGuide/GetFeature"
    },
    {
        "method_info": {
            "name": "ListFeatures",
            "is_client_stream": false,
            "is_server_stream": true
        },
        "name": "ListFeatures",
        "is_client_stream": false,
        "is_server_stream": true,
        "package": "main",
        "service": "RouteGuide",
        "full_method": "/main.RouteGuide/ListFeatures"
    },
    // ...
]

but yeah, I'm not sure if the anonymous struct reuse is worth it at this point 🤷‍♂️

Comment on lines 212 to 215
case int64:
timeout = time.Duration(float64(t)) * time.Millisecond
case float64:
timeout = time.Duration(t) * time.Millisecond
Copy link
Member

@na-- na-- Oct 20, 2020

Choose a reason for hiding this comment

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

See https://play.golang.org/p/gsOWwDPPAV3 for the rationale

Suggested change
case int64:
timeout = time.Duration(float64(t)) * time.Millisecond
case float64:
timeout = time.Duration(t) * time.Millisecond
case int64:
timeout = time.Duration(t) * time.Millisecond
case float64:
timeout = time.Duration(t * float64(time.Millisecond))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Very interesting.

Comment on lines 351 to 354
case int64:
timeout = time.Duration(float64(t)) * time.Millisecond
case float64:
timeout = time.Duration(t) * time.Millisecond
Copy link
Member

Choose a reason for hiding this comment

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

See https://play.golang.org/p/gsOWwDPPAV3 for the rationale

Suggested change
case int64:
timeout = time.Duration(float64(t)) * time.Millisecond
case float64:
timeout = time.Duration(t) * time.Millisecond
case int64:
timeout = time.Duration(t) * time.Millisecond
case float64:
timeout = time.Duration(t * float64(time.Millisecond))

Copy link
Member

@na-- na-- left a comment

Choose a reason for hiding this comment

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

Besides the few minor nitpicks, LGTM! 🎉 Awesome 🎊

imiric
imiric previously approved these changes Oct 20, 2020
Copy link
Contributor

@imiric imiric left a comment

Choose a reason for hiding this comment

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

Stellar work @rogchap! 👏

I don't have any gRPC experience, but this was easy to read (minus the vendor changes 😆) and use. Don't have any code comments, just spotted a couple of typos.

Though would you mind doing some cleanup/squashing of commits before merging?

js/modules/k6/grpc/client.go Outdated Show resolved Hide resolved
js/modules/k6/grpc/client.go Outdated Show resolved Hide resolved
@na--
Copy link
Member

na-- commented Oct 20, 2020

Regarding squashing - I was thinking we can squash this in just a single commit when merging the PR, through the GitHub interface? I know it has 250k LoC difference, but 99% of that is vendor/ changes... The actual k6 changes are very tidy and self-contained, so I don't think we'd loose anything if we squash them. I don't actually see a nice way to split them in multiple commits 🤷‍♂️

And if @rogchap doesn't modify the commits and force push in this PR, we would be able to look up the change history or the rationale behind some code here, just in case. Sort of the best of both worlds with this type of a change, I think?

So... here's my suggestion on how to proceed:

  1. @rogchap merges the latest master in here, again, since we changed some module versions and httpmultibin (sorry! 😞 ), no force pushes or commit cleanup/squashing
  2. fixes the minor nitpicks and typos we've found
  3. we give it a final 👍 and squash+merge it in master?
  4. party 🎉

Any objections?

btw, regarding the actual gRPC module name - we still haven't decided it. So we'll likely merge this PR with the current k6/grpc name, and rename it to k6/protocols/grpc (or whatever we decide) in a separate small PR before we release k6 v0.29.0. This way you won't have to rebase it 10 more times before we make our minds up 😅

@rogchap
Copy link
Contributor Author

rogchap commented Oct 20, 2020

Only a few commits in this PR 😉
I'm especially fond of the number of commits with this message: "the linter hates me" 🤣

Sounds like a plan; for what it's worth: in our company we have set the GitHub settings in all repos to only allow squash+merge from our PRs, so seldom need to manually squash/rebase.

Will address all comments; 11pm here in the future (Sydney) so will take a look in the morning.

@na--
Copy link
Member

na-- commented Oct 20, 2020

👍 Sounds good! btw for now we're tentatively going with k6/protocols/grpc as the module name, so please change it (and the samples/grpc.js example) as well.

@rogchap rogchap dismissed stale reviews from imiric and mstoykov via 512d34f October 21, 2020 03:13
@rogchap
Copy link
Contributor Author

rogchap commented Oct 21, 2020

Ok will update to k6/protocols/grpc

Just my 2¢ on the subject:

  • I like short import/require statements like k6/http, k6/grpc (ironic as Go imports tend to be long)
  • I understand the need for new versions of http and others, but it's unclear to a user that k6/http is old and k6/protocol/http is the new
  • what would happen if in the future you have a 3rd version of k6/http etc?
  • Have you considered version numbers similar to Go and Protocol Buffers? eg. k6/v2/http or even k6/http/v2
  • If I had to choose, my preference (in order) would be k6/proto/grpc then k6/protos/grpc, k6/protocol/grpc and finally k6/protocols/grpc

@na--
Copy link
Member

na-- commented Oct 21, 2020

I like short import/require statements like k6/http, k6/grpc (ironic as Go imports tend to be long)

I can sympathize, but we'd like to support a lot of other protocols (e.g. k6/protocols/kafka, k6/protocols/sql, k6/protocols/dns, k6/protocols/tcp, etc.), and also have several non-protocol k6 modules, which would probably occupy the shorter paths. For example, k6/execution (#1320), k6/metrics (already exists, but it will have additions with #1321), k6/data (#1539), etc. (those are probably not the final names).

I understand the need for new versions of http and others, but it's unclear to a user that k6/http is old and k6/protocol/http is the new

We will support k6/http for a long, long time, even when we have a new version. We might start emitting deprecation warnings, but we can't kill the old API, since 99% of existing scripts use it... We're likely just going to re-implement the old k6/http as a JS wrapper around the new one, but we'll see. It's still early to tell, since we keep delaying that new API in favor of other k6 features.

what would happen if in the future you have a 3rd version of k6/http etc?
Have you considered version numbers similar to Go and Protocol Buffers? eg. k6/v2/http or even k6/http/v2

This is one of the main issues we're struggling right now in our internal discussions... The other one is how to make clear to users that an API's stability is alpha (i.e. the API change or even disappear in future k6 versions) or beta (i.e. the API will stick around, but might still change a little before it stabilizes). We can document these things in the release notes and the documentation, but a lot of people just don't read these things... 😞

The current 2 top contender strategies are versioned paths, like you proposed (k6/protocols/http/v3, k6/protocols/grpc/beta, etc.), and feature flags/gates (a bit like kubernetes, so something like k6 run --features='grpc=v2,http=v1'). I currently lean towards the feature flags slightly, since they'd allow us to gradually add new (i.e. potentially unstable) features in an easy and clear way, and more or less force us to deprecate old ones, while at the same time allowing us to do it very gracefully.

The biggest drawback to versioned paths for me, especially if we go with /alpha / /beta for the initial module versions, is that this is basically the only guaranteed way that users will have to change their scripts. But it's also much less opaque than the feature gates - having global flags that modify the script behavior is a bit of an anti-pattern (which we suffer from in other places in k6 already).

Copy link
Member

@na-- na-- left a comment

Choose a reason for hiding this comment

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

🎉

Copy link
Member

@na-- na-- left a comment

Choose a reason for hiding this comment

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

🎉

Copy link
Contributor

@imiric imiric left a comment

Choose a reason for hiding this comment

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

Thank you!

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.

Support for gRPC protocol
9 participants