Objective 1: Identify and correct errors in the HealthCheck function #1
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Originally created on 18 August 2024 at 18:24 GMT+2
This PR brings a number of quick changes to fix bugs that leads to erroneous results. Those changes are clearly separated in small reviewable commits, which would normally be opened as separate PRs, but are included here together in the context of this challenge.
1. Fix loop variable capture issue (8ee6eb0)
This commit fixes the common Go pitfall of loop variable capture. Before Go 1.22 (released on February 6, 2024), the current code would print the following:
Each iteration of the
urls
loop uses the same instance of the variableurl
, so the goroutines share that single variable, resulting in using the last value of the slice. This problem can be detected withgo vet
:To fix it, the recommended solution is to simply bind the loop variable to a new variable inside the loop, either by creating and assigning a new
url
or by passing a copy as an argument of the anonymous goroutine function.This fix is not needed anymore since Go 1.22, but given that the current
go.mod
file specifiesgo 1.18
, I elected to still include it in this PR.2. Fix Results slice data race (1e41f54 & a1122f9)
The code include a classic case of a data race, where the
results
slice is read and written by the multiple goroutines concurrently.This problem can be detected with
go run -race
:Multiple solutions exist:
results = append(results, result)
with async.Mutex
lock & unlock.results
slice using the loop index, instead of appending.Each of those solution have their pros and cons, but they don't make much difference for small test samples. The usual idiomatic approach in Go would probably be to use a channel, but it does require a bit more code. This is why I initially opted to use a mutex. The code is simple to read and understand, and we can clearly see that the goal is to prevent a data race.
One issue I quickly noticed is that the order of the
urls
input is not preserved in theresults
output. This made things more complicated when implementing theTestHealthCheck
function, so I ended up going with the index write solution. I find that it's not necessarily the most explicit as to why it's done that way, so it's important to add a comment to explain the reasoning.3. Close & drain the http.Get response body (eb7f03e)
The
http.Get
function documentation clearly states:The first step is to close the response body, even if we don't use it.
This problem can be detected with the third-party tool
github.com/golangci/golangci-lint
and thegithub.com/timakin/bodyclose
linter:But that comment also technically says "when done reading from it". And the documentation of
http.Response.Body
states:Consequently, a second "optional" step is to drain it (meaning reading it to discard it). It's not strictly necessary in our use case, as our sample file of services will typically only have different URLs, so we don't need to worry about reusing TCP connections. Still, it's a good practice to do it, and we can simply read a limited amount of bytes to prevent potential performance issues.
Some resources:
4. Implement TestHealthCheck (c453f46)
The unit test is implemented by creating multiple mock servers, each for a status code of 201/308/404/500 + an invalid URL test case. The latency is based on the status code in milliseconds. Given that the order of the input is preserved, we cam then loop through the results to check that the values are what we expect.
Other potential improvements:
http.Get
method uses theDefaultClient
of thehttp
package. This client doesn't specify a timeout value, so a problematic server could cause the program to hang indefinitely. Creating a customhttp.Client
with a timeout value would fix this issue.http.Get
error handling doesn't set theUrl
field of theResult
struct. I wasn't sure if it was on purpose or not since the error message usually includes the URL, so there's still feedback to the user.