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

Refactor and add tests for Item client methods #119

Merged
merged 38 commits into from
Feb 9, 2021
Merged

Conversation

joe94
Copy link
Member

@joe94 joe94 commented Feb 4, 2021

No description provided.

@joe94 joe94 linked an issue Feb 4, 2021 that may be closed by this pull request
@codecov
Copy link

codecov bot commented Feb 4, 2021

Codecov Report

Merging #119 (a09a088) into main (faedd84) will increase coverage by 11.31%.
The diff coverage is 69.11%.

Impacted file tree graph

@@             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     
Impacted Files Coverage Δ
chrysom/store.go 0.00% <ø> (ø)
store/provide.go 0.00% <0.00%> (ø)
chrysom/client.go 73.94% <76.23%> (+73.94%) ⬆️
store/transport.go 85.57% <84.61%> (-1.03%) ⬇️
store/inputValidation.go 91.66% <100.00%> (+1.19%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update faedd84...6198207. Read the comment docs.

@joe94 joe94 changed the title Feature/better testing Refactor and add tests for Item client methods Feb 4, 2021
@joe94 joe94 added this to the v0.3.10 milestone Feb 4, 2021
@joe94 joe94 force-pushed the feature/betterTesting branch from 7372a61 to 5891bca Compare February 4, 2021 22:09
Copy link
Contributor

@kristinapathak kristinapathak left a 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.

Comment on lines 52 to 54
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")
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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

Comment on lines 114 to 115
func NewClient(config *ClientConfig) (*Client, error) {
err := validateConfig(config)
Copy link
Contributor

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?

Copy link
Member Author

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.

@@ -129,140 +162,229 @@ func validateConfig(config *ClientConfig) error {
}
return nil
}
func shouldUseJWTAcquirer(options acquire.RemoteBearerTokenAcquirerOptions) bool {
Copy link
Contributor

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 Show resolved Hide resolved

if config.Auth.Basic != "" {
return acquire.NewFixedAuthAcquirer(config.Auth.Basic)
func validateGetItemsInput(input *GetItemsInput) error {
Copy link
Contributor

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.

Copy link
Member Author

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 Show resolved Hide resolved
Comment on lines 232 to 240
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
}
Copy link
Contributor

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?

Copy link
Member Author

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 💯


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) {
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Member Author

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

Copy link
Contributor

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.

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 Show resolved Hide resolved

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) {
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Responded in comment above.

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.

@joe94 joe94 linked an issue Feb 4, 2021 that may be closed by this pull request
chrysom/store.go Outdated

Start(ctx context.Context) error
Start(ctx context.Context, input *GetItemsInput) error
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, good catch 👍

@joe94 joe94 modified the milestones: v0.3.10, v0.3.11 Feb 6, 2021
Copy link
Contributor

@kristinapathak kristinapathak left a 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()
}
Copy link
Contributor

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.

Copy link
Member Author

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

@joe94 joe94 mentioned this pull request Feb 9, 2021
@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 9, 2021

@joe94 joe94 merged commit 9d1bc42 into main Feb 9, 2021
@joe94 joe94 deleted the feature/betterTesting branch February 9, 2021 17:22
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.

Improve error handling in chrysom Add unit tests to chyrsom
4 participants