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

gRPC Client Server #18

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

gRPC Client Server #18

wants to merge 12 commits into from

Conversation

berkeli
Copy link
Owner

@berkeli berkeli commented Oct 25, 2022

No description provided.

@berkeli berkeli linked an issue Oct 26, 2022 that may be closed by this pull request
berkeli pushed a commit that referenced this pull request Oct 28, 2022
Copy link

@illicitonion illicitonion left a comment

Choose a reason for hiding this comment

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

This is really nicely put together, good job!

grpc-client-server/prober/prober.proto Outdated Show resolved Hide resolved
grpc-client-server/prober_client/main.go Outdated Show resolved Hide resolved
grpc-client-server/prober_client/main.go Outdated Show resolved Hide resolved
grpc-client-server/prober_client/main.go Show resolved Hide resolved

func CreateProgressBar(w io.Writer, timeout time.Duration, endpoint string, results <-chan *Result, wg *sync.WaitGroup) {
defer wg.Done()
ticker := 200 * time.Millisecond

Choose a reason for hiding this comment

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

If I set my timeout to 1s, what will I see the progress bar do?

Copy link
Owner Author

Choose a reason for hiding this comment

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

It's not ideal at the moment, it doesn't show the progress of requests but only the progress of timeout.

So if you provide 1s timeout it will have 5 ticks (20%, 40%, etc). If the request completes in 0.5s then it will just fill up to 100%.

In that sense, I guess it's more of a countdown than a progress bar :D

Choose a reason for hiding this comment

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

Will the first tick actually hit 20% though? How does the progress bar know that 1 tick is 20%?

Copy link
Owner Author

@berkeli berkeli Nov 2, 2022

Choose a reason for hiding this comment

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

So the progress bar starts with 0, then for ticker I have provided 200ms.

100% for the progress bar is int(timeout/ticker) for 1s this will be 5, so progress bar will start at 0 then 20%(1/5), 40%(2/5) etc.

The ticker doesn't add 200ms, it only adds 1

case <-time.After(ticker):
			bar.Add(1)

grpc-client-server/prober_client/main_test.go Outdated Show resolved Hide resolved
}
}

var lis = bufconn.Listen(1024 * 1024)

Choose a reason for hiding this comment

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

I don't think this is used?

value: "foo",
want: &ArrayFlag{"foo"},
},
"add another element": {

Choose a reason for hiding this comment

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

I'd say "add two elements" rather than "add another element" - another implies it's related to something else that already happened (e.g. that your two test cases depend on each other, which they don't)

Copy link
Owner Author

Choose a reason for hiding this comment

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

It's actually adding another element, there's no option to add multiple at once

Choose a reason for hiding this comment

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

The difference I'm trying to highlight is that if you have two test cases: "add element" and "add another element", they read as if possibly related.

Can you think of a name which doesn't suggest that?

})
}

func TestGRpc(t *testing.T) {

Choose a reason for hiding this comment

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

It's really useful to have these tests which test your test helpers (i.e. non-production code you're just using for tests), but often we'd put these in their own test file so it's easier to see at a glance what tests you have for your actual application vs test helpers.

resp, err := client.DoProbes(context.Background(), tc.req)

if err == nil {
require.NotNil(t, resp.AverageResponseTime)

Choose a reason for hiding this comment

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

Can you think how you could actually test the response time as well?

(hint: you probably don't actually want to connect to Google when doing so!)

Copy link
Owner Author

Choose a reason for hiding this comment

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

great suggestion, I have added a test now.

There could be another way of testing it, by extracting the prober and making it take a parameter for time object. I went with a solution where I mock time.Since, as I think it's clearner for this small usecase.

Choose a reason for hiding this comment

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

The mock time.Since works, but the golang HTTP stack has a built-in type for this (just like you used a custom Dialer, called an http.RoundTripper - try using it :)

Copy link

@illicitonion illicitonion left a comment

Choose a reason for hiding this comment

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

Nice improvements!

}

if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return ttfb, ttlb, fmt.Errorf("status code %d", resp.StatusCode)

Choose a reason for hiding this comment

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

What value will ttlb have here? Is that an accurate/useful value? Is there a better value we could use?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes using a null object would defenitely be best practice.

_, err = io.ReadAll(resp.Body)

ttlb = timeSince(start)
resp.Body.Close()

Choose a reason for hiding this comment

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

You should make sure to close the body, even if you don't care about its contents.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes, but which one would be true TTLB? closing the body and then measuring time or measuring time and then closing body. There's 1-2ms difference in some cases

Choose a reason for hiding this comment

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

That's an interesting question (to which I'm actually not sure the answer - probably before the close makes more sense), but I was more thinking about not closing the body in the case of your early-return based on the status code :)

resp, err := client.DoProbes(context.Background(), tc.req)

if err == nil {
require.NotNil(t, resp.AverageResponseTime)

Choose a reason for hiding this comment

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

The mock time.Since works, but the golang HTTP stack has a built-in type for this (just like you used a custom Dialer, called an http.RoundTripper - try using it :)

ttlbTotal += ttlb
}
ttfbAverage := time.Duration(float32(ttfbTotal) / float32(in.NumberOfRequests))
ttlbAverage := time.Duration(float32(ttlbTotal) / float32(in.NumberOfRequests))

Choose a reason for hiding this comment

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

Do you think we want to average together the TTLB for both successful and unsuccessful responses?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Ah good find, we should defenitely not take them into account!

Copy link

@illicitonion illicitonion left a comment

Choose a reason for hiding this comment

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

Looking great! A few final thoughts :)

grpc-client-server/prober_server/main.go Outdated Show resolved Hide resolved
grpc-client-server/prober_server/main.go Outdated Show resolved Hide resolved
Comment on lines 129 to 132
type TimedRoundTripper struct {
defaultTripper http.RoundTripper
recordTime func(time.Duration)
}

Choose a reason for hiding this comment

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

This is a pretty common pattern we run into in code, to have an implementation of an interface which wraps another, doing something special around it and using it - often called the decorator pattern.

Rather than calling the field here defaultTripper, we'd ordinarily call it something like delegate or underlying. The reason not to call it default is that we don't actually know or care that the one we delegate to is the http.DefaultTransport - maybe the one we delegate to also has some other special behaviour.

Copy link
Owner Author

Choose a reason for hiding this comment

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

That's great, Thanks!


resp, err := client.DoProbes(context.Background(), tc.req)

if err == nil {

Choose a reason for hiding this comment

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

We shouldn't be conditionally making assertions based on whether our real implementation returned an error - we should be asserting whether we expect the real implementation to return an error.

Copy link
Owner Author

Choose a reason for hiding this comment

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

I had created a field in test cases for errors, no idea why I didn't use them. Thanks!


client := pb.NewProberClient(conn)

timeSince = func(t time.Time) time.Duration {

Choose a reason for hiding this comment

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

Rather than setting a global variable here, can you make it so that when you construct your server, you pass in this value? (Or actually, you pass in the http.RoundTripper implementation you want to use? Think about how the http.Client struct works - if you construct one with a custom RoundTripper, it'll use it, if not, it'll use a default one).

That way, in your test you can pass in a custom RoundTripper without needing any global variables.

Copy link
Owner Author

Choose a reason for hiding this comment

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

This is great, thanks! implemented now with timeSince mock for server and http client.

Not super happy with the implementation that client is referencing a variable from the parent function, will have to do some refactoring to remove that dependency and you suggesting of passing a custom transport to the server sounds like it might solve this issue..

	ttfbTotal := time.Duration(0)
	ttlbTotal := time.Duration(0)
	failed := 0
	latestTtfb := time.Duration(0)

	c := &http.Client{
		Transport: &TimedRoundTripper{
			underlying: http.DefaultTransport,
			recordTime: func(t time.Duration) {
				latestTtfb = t
				ttfbTotal += t
			},
			timeSince: s.timeSince,
		},
	}

Comment on lines 95 to 96
require.Greater(t, resp.TtfbAverageResponseTime.AsDuration(), time.Duration(0))
require.Equal(t, 2*time.Second, resp.TtlbAverageResponseTime.AsDuration())

Choose a reason for hiding this comment

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

One of these assertions is really specific, and the other really vague - what makes it hard to be specific in both?

Copy link
Owner Author

Choose a reason for hiding this comment

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

The challenge was to do with 2 different timing methods, 1 for TTFB and 1 for TTLB and that they are in 2 different places.

I didn't want to measure TTLB inside the roundtripper as it will read everything from the body and return empty body. For our use case this would be fine, but I still think it's bad practice to be modified something that's expected to not be modified.

We could of course make a copy of the body and return that, but this would use up unwanted resources.

I have now implemented a solution for precise testing, which required using timeSince mock for both the server and Roundtripper.

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.

gRPC project
2 participants