-
Notifications
You must be signed in to change notification settings - Fork 881
chore: add usage information to clock library README #13594
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
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. Join @spikecurtis and the rest of your teammates on |
_Note: Quartz is the name I'm targeting for the standalone open source project when we spin this | ||
out._ |
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 know when this would be? Is it worth calling this quartz
here instead 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.
I think it will be soon. We could rename to quartz
in-repo before spinning out, or at the time we spin out. I don't think one is particularly easier or harder.
} | ||
``` | ||
|
||
The `*Mock` clock starts at Jan 1, 2024, 00:00 UTC by default, but you can set any start time you'd like prior to your test. |
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.
2024
will quickly be arbitrary and difficult to remember. How about the year 2000?
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.
More or less still arbitrary. If the start time matters at all to the test best practice is to Set()
it at the start.
distinguish them in your traps. | ||
|
||
```go | ||
trap := mClock.Trap.Now("foo") // traps any calls that contain "foo" |
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 find tag systems have a persistent cognitive overhead as I have to remember the subtle differences between implementations. E.g.:
- do tags match exactly or does a subset suffice
- if I trap "foo" and call w/ "foo" "bar" does it match?
- if I trap "foo" "bar" and call w/ "bar" does it match?
- is matching case sensitive?
And then, there's a long-term burden of keeping the tag names consistent.
Instead, I wonder if we should promote accepting multiple independent clock instances.
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 wonder if we should promote accepting multiple independent clock instances.
I think that would significantly increase the friction of adopting this library. In simple cases, you should be able to get by without using tags at all. In more complex cases, having to manage multiple clocks could quickly become burdensome. I could imagine folks ending up just keeping a map[string]clock
to keep track of them all, which amounts to much the same thing?
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 was thinking using different variables, so leveraging the language instead of a prescribed data structure.
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 cognitive overhead of thinking about multiple clocks sounds way harder than tags. I'm strongly against promoting it in our documentation, but there is nothing actually stopping people from doing it if tags feels that burdensome.
We could make the tag matching more explicit in our naming, at the expense of a more verbose API. I.e.
trap := mClock.Trap().Now().Contains("foo")
rather than our present
trap := mClock.Trap().Now("foo")
It would be more extensible for additional matching strategies in future. I personally prefer to type less in UTs and handle the "cognitive burden", but I'm open to changing it if that's an overall consensus.
Keep in mind that this is a library for unit testing, and as such, the number of unique calls is typically a handful, perhaps a few dozen. I don't think it's particularly good practice to mock time for integration tests involving more than a few components.
Tagging (labeling) systems like K8s are designed to handle clusters with tens of thousands of containers and so have a complex matching language. Honestly for this library, just having a single string with exact matching would be sufficient for 95% of cases, but
- the
...string
syntax affords multiple entries, and it felt annoying to like,panic
if you put more than one - there are some few use cases for multiple tags, such as components that programmatically start multiple tickers/timers, such as apphealth, which then allows us to verify tickers for each app get started.
c7079dc
to
39aa737
Compare
edbd4ed
to
fcecc22
Compare
39aa737
to
d314742
Compare
fcecc22
to
38ec726
Compare
38ec726
to
82e85b4
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.
Nice work on the documentation!
|
||
Whenever you would call into `time` to start a timer or ticker, call `Component.clock` instead. | ||
|
||
In production, set this clock to `quartz.NewReal()` to create a clock that just transparently passes |
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 wonder if NewStdlib
or something like that would be clearer. I feel like NewReal
refers to a real-time clock (hardware). Maybe just me. 😅
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.
NewReal
makes sense to me in the context of a mock clock.
I find the reference toStd
or Stdlib
in API names to be a code smell.
d, w := mClock.AdvanceNext() | ||
w.MustWait(ctx) | ||
// d contains the duration we advanced | ||
``` |
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 might be good to document when and why you would use Advance
instead of AdvanceNext
. Advance
has basically the same limitation, except it can increment to times before any tick/timers.
To make Advance
more useful, I think it would be good to have a mClock.NextTick()
, this can allow you to write code like:
for i := 0; i < 10; i++ {
d := time.Second
if nt := mClock.NextTick(); nt < d {
d = nt
}
mClock.Advance(d).MustWait(ctx)
}
Thoughts? A use-case that is very hard to cover under mClock.Advance
is:
t := mClock.NewTicker(10 * time.Second)
for range t.C {
// do job that takes nondeterministic amount of time.
t.Reset(10 * time.Second)
}
PS. The argument could probably be made that this library is not meant for this kind of use-case, but then I question the utility of the Advance
function.
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 do think the NextTick()
call is useful, although I would probably call it Next()
or perhaps NextEvent()
, since it's not just tickers that are affected.
The use case I had in mind was being able to blow through timers and ticks to advance a certain duration.
desired := time.Minute
for desired > 0 {
n := mClock.Next()
if n > desired {
mClock.Advance(desired).MustWait(ctx)
break
}
mClock.Advance(n).MustWait(ctx)
desired -= n
}
I'm not sure I understand the point of your first example. Is it that you don't know the size of the ticks and you want to advance 10 seconds or 10 ticks, whichever comes first?
In terms of the second example, I'm pretty confident it works fine with Advance()
: you can trap the t.Reset()
call and control the amount of mocked time the job takes (the amount of real time should be irrelevant).
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.
Oh, and, I think Advance()
is usually preferable to AdvanceNext()
, since it allows you to keep a mental tally of time passing during the test case. But, not always. There was even one test I refactored that I thought was clearer using Set()
.
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 not sure I understand the point of your first example. Is it that you don't know the size of the ticks and you want to advance 10 seconds or 10 ticks, whichever comes first?
I based it off the example in readme, so I didn't really pay attention to the i < 10. Essentially the idea was to advance 10 seconds, one second at a time, or if an event is incoming, advance to it and then continue (since you can't advance past it). I believe your desired
example is closer to what I was going for.
In terms of the second example, I'm pretty confident it works fine with Advance(): you can trap the t.Reset() call and control the amount of mocked time the job takes (the amount of real time should be irrelevant).
I suppose you're right, since we can assume the clock is stopped unless we control it, we know when that Reset
will land. I wanted to demonstrate a case where we couldn't know the Reset
value but my example didn't quite reflect this. 😅
Oh, and, I think Advance() is usually preferable to AdvanceNext(), since it allows you to keep a mental tally of time passing during the test case. But, not always. There was even one test I refactored that I thought was clearer using Set().
I can totally get behind that reasoning, but in complex systems I think it can become hard to account for all the timers and ticks, esp. if they're of different lengths, or worse, not fully predictable (then again, that could be a sign of bad design).
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 added Next()
up stack from this, btw.
82e85b4
to
2ed3ff0
Compare
Removed my review as it seems like you're getting great FB from others and I don't want to hold up the release. |
2ed3ff0
to
878bf34
Compare
Adds a Usage section to the README of the clock testing library.