-
Notifications
You must be signed in to change notification settings - Fork 9
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
Refactor and add tests for Item client methods #119
Conversation
Codecov Report
@@ Coverage Diff @@
## main #119 +/- ##
===========================================
+ Coverage 43.19% 54.51% +11.31%
===========================================
Files 16 16
Lines 801 842 +41
===========================================
+ Hits 346 459 +113
+ Misses 437 358 -79
- Partials 18 25 +7
Continue to review full report at Codecov.
|
7372a61
to
5891bca
Compare
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'm pretty wary of putting pointers everywhere and I'm not sure what the benefits of these structs are.
chrysom/client.go
Outdated
ErrGetItemsFailure = errors.New("failed to get items. Non-200 statuscode was received") | ||
ErrRemoveItemFailure = errors.New("failed to delete item. Non-200 statuscode was received") | ||
ErrPushItemFailure = errors.New("failed to push item. Non-success statuscode was received") |
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.
Some of these errors make sense to be exported, but some of them don't seem specific enough to be useful? Usually I export an error because the consumer of the package could have different logic/functionality after being given the error. Are all of these useful in that way?
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 thought about making these more specific but they would blow up in number so I thought they'd still be helpful if returned in some form like this:
return nil, fmt.Errorf("statusCode %v: %w", response.Code, ErrPushItemFailure)
This will help when looking at tr1d1um logs and realizing Argus is returning a 403 instead of just a generic non-200 which I think is what you were getting at.
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 have no problem with error variables; my point is all of them are exported. The only reason to export an error is to provide consumers of this package a way to do different logic based on receiving the error:
err := doSomething()
// special flow
if errors.Is(err, ErrNotFound) {
doSomethingElse()
return
}
// normal flow
chrysom/client.go
Outdated
func NewClient(config *ClientConfig) (*Client, error) { | ||
err := validateConfig(config) |
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 change means that now instead of validateConfig()
modifying NewClient
's copy of the config, it is modifying the config struct that the caller also has. We're good with that?
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.
Yeah, I think that's fine. Users should make a defensive copy if they intend to reuse the config.
chrysom/client.go
Outdated
@@ -129,140 +162,229 @@ func validateConfig(config *ClientConfig) error { | |||
} | |||
return nil | |||
} | |||
func shouldUseJWTAcquirer(options acquire.RemoteBearerTokenAcquirerOptions) bool { |
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.
Could make this shorter by calling it isNonEmpty()
or flip the logic and call it isEmpty()
.
you know, we could also just add an IsEmpty()
function to acquire.RemoteBearerTokenAcquirerOptions
....
chrysom/client.go
Outdated
|
||
if config.Auth.Basic != "" { | ||
return acquire.NewFixedAuthAcquirer(config.Auth.Basic) | ||
func validateGetItemsInput(input *GetItemsInput) 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.
This is a personal preference, but the ordering of functions in this file make it more difficult to see how the pieces fit together. For example, this function is defined far away from the functions that use it. Maybe it belongs with the GetItemsInput
struct? I had to go to another file to find out what we were validating.
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.
Yeah, good call. I'll move these to the paramStructs file.
chrysom/client.go
Outdated
request, err := c.makeRequest(input.Owner, http.MethodGet, URL, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
items := []model.Item{} | ||
err = json.NewDecoder(response.Body).Decode(&items) | ||
response, err := c.do(request) | ||
if err != nil { | ||
return nil, err | ||
} |
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.
with every makeRequest()
call immediately followed by do()
and neither having their error wrapped to provide more context, I don't see the point in having two separate functions? Can we just combine them into a function called sendRequest()
with the parameters that makeRequest()
takes?
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 created them separately as I wasn't sure what would happen in between but I now I think combining makes sense 💯
chrysom/client.go
Outdated
|
||
response, err := c.client.Do(request) | ||
// GetItems fetches all items in a bucket that belong to a given owner. | ||
func (c *Client) GetItems(input *GetItemsInput) (*GetItemsOutput, 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.
Accepting this as a pointer allows the client to modify the *GetItemsInput
. I know it isn't currently doing that, but it makes me nervous. What is the benefit of having a pointer here? Similarly, what is the benefit of having a struct? Why not take the two strings separately?
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 pretty much have the same issue with other Input
structs.
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 followed a similar pattern to what folks at AWS use for their dynamodb go sdk https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/#DynamoDB.PutItem
One big benefit is that it facilitates updating the client without changing the signature of the methods. We can deprecate new fields and add new ones in a backwards compatible way.
Even when our structs are not super large, PushItem(*PushItemInput)PushItemOutput
already ran around 4000ns faster / op than PushItemOutput(PushItemInput)PushItemOutput
goos: darwin
goarch: amd64
pkg: github.com/xmidt-org/argus/chrysom
BenchmarkPushItemByValue-12 13467 80400 ns/op 7841 B/op 100 allocs/op
PASS
ok github.com/xmidt-org/argus/chrysom 2.140s
goos: darwin
goarch: amd64
pkg: github.com/xmidt-org/argus/chrysom
BenchmarkPushItem-12 15087 77748 ns/op 7865 B/op 101 allocs/op
PASS
ok github.com/xmidt-org/argus/chrysom 2.248s
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.
Everything I research about passing a pointer vs value says that value is faster:
https://medium.com/a-journey-with-go/go-should-i-use-a-pointer-instead-of-a-copy-of-my-struct-44b43b104963
I often find that pointers are used by SDKs that are written in multiple languages...it doesn't feel very Go-like to me. Writing functions in this way seems very different from how we currently write code, and if we want to do this I think it should be a team decision. Personally, I don't think the benefits of potential future backwards compatibility makes up for the potential for bugs, additional code maintenance, and larger mental model needed to understand the code (now I cannot simply look at the function signature and understand what's going on - I have to go look up what these structs are). But if this is the direction the team wants to move in, then we can.
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.
*GetItemsOutput
is a pointer to a struct that only has a slice, which is a pointer. Why can't we just return the slice?
The decision here is more about how the API will evolve. It feels to me like this method won't ever need to return anything but a slice, so have a full-on struct
as a result type seems unnecessary. I'm not as close to the design, though.
Don't forget that you can take the middle ground and return a custom slice type:
type Items []Item
func GetItemsOutput(...) Items
That has a few advantages:
(1) You can now introduce methods on your return type Items
. That helps the API evolve over time, as you can introduce additional behavior without having to change the GetItemsOutput
function.
(2) You can document the Items
type with godoc
(3) You can switch to a type Items struct {...}
in the future without too much of a breaking change. Since clients won't typically instantiate the Items
type themselves, it won't break very much code.
chrysom/client.go
Outdated
|
||
response, err := c.client.Do(request) | ||
// GetItems fetches all items in a bucket that belong to a given owner. | ||
func (c *Client) GetItems(input *GetItemsInput) (*GetItemsOutput, 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.
*GetItemsOutput
is a pointer to a struct that only has a slice, which is a pointer. Why can't we just return the slice?
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 have the same question about other Output
structs.
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.
Responded in comment above.
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.
Passing by value seems better in this instance.
I would second what @kristinaspring says above. I'm not so much worried about speed of a particular call. Over time and at scale, the garbage collector performance tends to dominate. Passing by value removes the garbage collector from that part of the callstack, at least insofar as the parameters are concerned.
It's best to make the decision to pass by value or pointer based on API design rather than performance. If something is immutable or shouldn't be modified by a function, passing by value is a great way to communicate that without having to put a lot of verbiage in godoc
. Alternatively, if the function does or may modify something, passing by pointer communicates that succinctly as well.
Obviously, if you benchmark and find significant performance gains by switching to pointers, by all means pass by pointer. But 4000ns/op, or in other words 4 microseconds/op, doesn't seem all that huge.
chrysom/store.go
Outdated
|
||
Start(ctx context.Context) error | ||
Start(ctx context.Context, input *GetItemsInput) 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.
This no longer matches the uber.fx hook struct for starting up an application.
https://pkg.go.dev/go.uber.org/fx#Hook
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 catch 👍
Co-authored-by: kristinaspring <kmspring57@gmail.com>
…to feature/betterTesting
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.
Looks great! 🚀
c.ticker.Stop() | ||
if c.observer != nil && c.observer.ticker != nil { | ||
c.observer.ticker.Stop() | ||
} |
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 doesn't need to be done in this pr, but should we add a WaitGroup
to ensure that the long running go routine has exited before returning? if GetItems()
takes time but is successful, we could be trying to update the listener after it is already shut down.
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.
Yeah, I think that's a great idea to avoid things sneakily happening in the background after Stop() returns 🚀
I'll add an issue for it
SonarCloud Quality Gate failed. |
No description provided.