-
Notifications
You must be signed in to change notification settings - Fork 322
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
chore: introduce in-memory stats for testing #2735
Conversation
Codecov ReportBase: 46.85% // Head: 46.74% // Decreases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## master #2735 +/- ##
==========================================
- Coverage 46.85% 46.74% -0.11%
==========================================
Files 298 299 +1
Lines 48943 49072 +129
==========================================
+ Hits 22931 22940 +9
- Misses 24555 24672 +117
- Partials 1457 1460 +3
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
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.
Overall looks good to me and I like that this would simplify our tests. Personally I find gomock to be very unfriendly and I tend to prefer moq a lot more. Not sure if with moq you'd still feel the need to do all this, perhaps it is worth checking before merging this (just in case since it might help with other cases too).
So mostly two things:
- please check moq, let me know if it can help somehow
- beware of reading
LastValue
directly in the tests. if some code is still running the write is protected by the library via a mutex but in the test you're reading it without passing through the mutex. it's usually fine in unit tests, less fine in integration tests where the code that you're testing might still be running while you make your assertions. that could cause a failure when running tests of that kind with-race
In any case just reading the last value in some cases feels a bit fragile so perhaps a race failure is OK as it would force us to review the test. Anyhow, this could be a good starting point, imo we can add more on it if needed down the road.
services/stats/memstats/stats.go
Outdated
if m.Type != stats.CountType { | ||
panic("operation Increment not supported for measurement type:" + m.Type) | ||
} | ||
|
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 check is redundant.
if m.Type != stats.CountType { | |
panic("operation Increment not supported for measurement type:" + m.Type) | |
} |
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.
Why do you say this check is redundant?
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.
Because right after this if
you're doing m.Count(1)
and the Count
method is already checking whether the stat is of the right type. We can remove this if
and just call Count
, right?
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 see; I did it this way cause the panic would be a bit misleading otherwise.
m.Increment()
Will panic with operation Count not supported for measurement type:" + m.Type
services/stats/memstats/stats.go
Outdated
if m.Type != stats.TimerType { | ||
panic("operation End not supported for measurement type:" + m.Type) | ||
} | ||
|
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 check is redundant.
services/stats/memstats/stats.go
Outdated
if m.Type != stats.TimerType { | ||
panic("operation Since not supported for measurement type:" + m.Type) | ||
} | ||
|
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 check is redundant.
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.
- please check moq, let me know if it can help somehow
Overall I prefer moq for generating mocks. I still find it will add unnecessary complexity to the alerts. The reason is stats and measurement are very coupled, the problem appears when you have to deal with multiple metrics. Essentially the logic that is implemented here should be repeated one way or the other, in all the test cases.
- Ensuring that the correct measurement (name, tags) is changed.
- Ignoring stats we are not interested in.
Thought I could have used the moq, to build the measurement implementation.
Given the frequency and complexity of testing stats, I would go ahead with this custom implementation. Maybe we should have the same for logging.
- beware of reading
LastValue
directly in the tests. if some code is still running the write is protected by the library via a mutex but in the test you're reading it without passing through the mutex. it's usually fine in unit tests, less fine in integration tests where the code that you're testing might still be running while you make your assertions. that could cause a failure when running tests of that kind with-race
In any case just reading the last value in some cases feels a bit fragile so perhaps a race failure is OK as it would force us to review the test. Anyhow, this could be a good starting point, imo we can add more on it if needed down the road.
Good point, it slipped my mind, I will make sure it is thread-safe.
services/stats/memstats/stats.go
Outdated
now: ms.Now, | ||
} | ||
|
||
ms.byKey[name+tags.String()] = m |
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.
Probably a overkilling but I'd generate they key with a function:
func (*Store) getKey(name string, tags stats.Tags) string {
return name+tags.String()
}
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 would be nice before merging it to use it in at least one test :)
|
||
var _ stats.Measurement = (*Measurement)(nil) | ||
|
||
type Store struct { |
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.
all fields in the Store
struct could be unexported and intialization could be taken care once and for all by a producer function using Opts
if necessary. This would eliminate the need for the init
method being called by other methods and the sync.Once
field e.g.
type Opt func(*Store)
var WithNow = func(now func() time.Time) Opt {
return func(s *Store) {
s.now = now
}
}
// New returns a new stats.Stats implementation that stores stats in-memory.
func New(opts ...Opt) *Store {
s := &Store{
byKey: make(map[string]*measurement),
now: time.Now,
}
for _, opt := range opts {
opt(s)
}
return s
}
type Store struct {
mu sync.Mutex
byKey map[string]*measurement
now func() time.Time
}
and other packages could simply do
store := memstats.New(
memstats.WithNow(func() time.Time {
return now
}))
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.
Your suggestion and my implementation are good patterns to initialise components in Go. I use and like both depending on the situation.
I don't see it as a problem having unexported fields for options; keep in mind most standard libraries are using this pattern.
The downside of the lazy init approach is that we have to call .init
in every exported method. The with
approach also has an overhead of as well but is safer indeed.
@atzoum I would added it here If @achettyiitr likes it. I don't want to couple these changes in the same PR, given it addressing a different audience and they have different priorities. |
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.
@lvrach I replied to the "redundant check" comment. if it makes sense please make the changes. overall again, it looks good. thanks for the PR 👍
Thanks, @lvrach for the PR. |
3ee8df1
to
0daa80e
Compare
Description
This PR introduces an implementation of
stats.Stats
andstats.Measurements
, where the metrics are kept in a simple in-memory store to assist with testing stats side-effects.Motivation
The current method of testing stats is to use an auto-generated mock:
and latter
Apart from this approach being a bit complex, the primary issue is the fragility and extensibility of this approach.
Imagine a new stats is introduced, which shouldn't be uncommon. The assertion above will fail and to fix it, you need to do something like:
You can easily understand how those tests can become unmanageable.
Proposed solution
This approach has the following benefits:
name
, andtags
should match our expectations.Limitations
Notion Ticket
https://www.notion.so/rudderstacks/memory-stats-for-tests-2846f156181f4843a848471660529d61
Security