jonboulle / clockwork

a fake clock for golang

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Synchronous Timers?

MichaelSnowden opened this issue · comments

Is there an option to make it so that the callbacks from timers are invoked synchronously instead of in a goroutine? Essentially, remove go from here:

go f.afterFunc()

My issue with the current method is demonstrated by this workaround https://github.com/temporalio/temporal/blob/15974517a5ea2987a7f9a02afefc423418ba658a/service/matching/liveness_test.go#L44

Because Advance is not synchronous, and we are trying to verify that the callback is not fired, there is no event for us to actually wait on in the test, so we have to resort to a time.Sleep.

As a thought experiment, should this be the default behavior?

Most people are probably using channels as side-effects to wait for the goroutine to start/end. This will be racey on slow machines in all circumstances. It is also more verbose and, frankly, annoying. Default synchronous behavior would alleviate that, IIAUC.

However, if a user is using AfterFunc to start a never-ending function, Advance will never return. Not great, but this feels like a very rare use case.

Another thought, are we looking at this the right way? What if instead of changing behavior we gave callers what they need?

What if we added an expiration/firing count that users can just read? Then instead of worrying about behavior modifications you can just read a counter.

As a thought experiment, should this be the default behavior?

So, I think having this be the default behavior would be perfect because users could just spawn a goroutine in the callback if they need to do something that takes a while. Basically, providing a synchronous API, users can choose to do sync or async, but providing an async API, they're stuck with that. However, I don't want to break existing users who are relying on this behavior.

Another thought, are we looking at this the right way? What if instead of changing behavior we gave callers what they need?

What if we added an expiration/firing count that users can just read? Then instead of worrying about behavior modifications you can just read a counter.

I thought about something like this with a Flush method that looks at a WaitGroup which tracks pending callbacks. However, I think it would be annoying because you'd have to add Flush() to the end of every call that you need to make synchronous. If all calls are synchronous, then you can just switch between Advance and go Advance depending on your needs. It would also allow you to interleave synchronous and non-synchronous calls because, with a wait group or similar counting mechanism, there's be no way to do something like go Advance(), Advance().

you can just switch between Advance and go Advance depending on your needs.

I don't think this needs to be the case. API guarantee was just ~"the counter only changes by calling Advance, and only increases by the number of expired counters, regardlss of type." There should be no need to call go FakeClock.Advance()

I'm going to type to throw together a PR to see what this looks like. It should be very doable without adding new behavior dynamics.

TLDR: @MichaelSnowden Can you take a look at #69 and let me know

  1. Will that meet your needs?
  2. If not, why not?

Longer musing

I should have read and responded to this quote a little more closely before I responded above, so I am doing so now.

I thought about something like this with a Flush method that looks at a WaitGroup which tracks pending callbacks.

If I am understanding you correctly, isn't tracking pending callbacks is specifically what you want to avoid? In an attempt to summarize the problem, there are only 2 cases we need to cover:

  1. Did a function/behavior complete?
  2. Did the FakeClock start the execution a function/behavior (or not)? This is only difficult when one is proving that AfterFunc did not fire, because other operations are channel-based and deterministic around Advance. You can read from channels and synchronously verify.

Item 1 is on the caller to track via completion channels or side effects. It isn't clockwork's job to reason about whether a caller-defined thing completed. Example:

fc := clockwork.NewFakeClock()
ch := make(chan bool)
fc.AfterFunc(time.Second, func(){
  time.Sleep(time.Minute)
  close(ch)
})
fc.Advance(time.Second)

<-ch // waits a minute for completion of the function

Item 2 is is the focus of your ask here, becuase there is currently no way to validate that the AfterFunc function didn't fire.

I have struggled with this myself, and in hingsight I really should have offered a way to do this earlier. I believe #69 achives this.

I actually think this is an issue worth finding a solution for, but I don't personally mind it and no one has chimed in (@MichaelSnowden or anyone else) for a while.

I'll close this on 1 Nov unless someone comments that it is worth pursuing.