-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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!
|
||
func CreateProgressBar(w io.Writer, timeout time.Duration, endpoint string, results <-chan *Result, wg *sync.WaitGroup) { | ||
defer wg.Done() | ||
ticker := 200 * time.Millisecond |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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%?
There was a problem hiding this comment.
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)
} | ||
} | ||
|
||
var lis = bufconn.Listen(1024 * 1024) |
There was a problem hiding this comment.
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": { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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!)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this 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 :)
type TimedRoundTripper struct { | ||
defaultTripper http.RoundTripper | ||
recordTime func(time.Duration) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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,
},
}
require.Greater(t, resp.TtfbAverageResponseTime.AsDuration(), time.Duration(0)) | ||
require.Equal(t, 2*time.Second, resp.TtlbAverageResponseTime.AsDuration()) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
No description provided.