Is there a way to use `TestCoroutineScheduler.adva...
# coroutines
m
Is there a way to use
TestCoroutineScheduler.advanceTimeBy()
and get a callback whenever the
currentTime
changes in order to execute side-effects? Or at least query the next event time?
d
No such mechanisms at the moment, and you're the first one to my knowledge to request it.
m
Interesting. We need to simulate more than just coroutine timing - for example a job scheduler. So we need to have all clock simulations (coroutine scheduler, wall clock) in sync and run all side-effects of advancing time - not just coroutines.
d
If I understood correctly, this is a tough problem, and, in general, a callback would not be enough. Let's say there are three schedulers (
X
,
Y
, and
Z
) that need to have virtual time. Also, let's, as you propose, designate the time as seen by
X
as the real time.
Y
is registered as the first thing to get a callback for a time change from
X
,
Z
is registered as the second thing. •
X
has a task scheduled in 10 seconds, •
Y
has a task scheduled in 9 seconds, •
Z
has a task scheduled in 2 seconds.
X
proceeds to grab another task from the queue and announces: "The virtual time is now 10".
Y
urgently runs the task, and then,
Z
starts running its task. We're left with an insane and completely unrealistic interleaving. The Proper™ thing to do here would be to have a queue of tasks shared by all three things and advance the virtual time for all of them. Here, the knowledge of when the next task is planned would help you, that's a nice use case for that. The Proper™ thing may be too difficult to do though, depending on the provided APIs of
X
,
Y
, and
Z
, so maybe something like this could be the best way instead:
Copy code
val timerResolution = 50.milliseconds
while (testNotFinished) {
  X.advanceTimeBy(timerResolution)
  Y.advanceTimeBy(timerResolution)
  Z.advanceTimeBy(timerResolution)
}
m
Thank you Dmitry. Interesting idea. Using a certain resolution could certainly work reasonably well (we’d have 630,720,000 iterations for one year). Your example assumes that X, Y and Z all behave like the test scheduler. For example I know in advance the next event timestamp for the job scheduler. It’s just the test coroutine scheduler that keeps such info private. Something like that might also work if the test coroutine scheduler would expose info about the next pending event time:
Copy code
fun advanceTimeBy(delay: Long) {
   var remainingDelay = delay

   while (true) {
      val delayToNextEvent = listOfNotNull(X.delayToNextEvent, Y.delayToNextEvent, Z.delayToNextEvent)
         .minOrNull()
         ?.takeIf { it <= remainingDelay }
         ?: break
      X.advanceTimeBy(delayToNextEvent)
      Y.advanceTimeBy(delayToNextEvent)
      Z.advanceTimeBy(delayToNextEvent)

      remainingDelay -= delayToNextEvent
   }
}
d
There's a missing
X.advanceTimeBy(remainingDelay)
and such at the end so the virtual time is properly updated, but other than that, yeah, I agree that this would also work… unless someone is adding new tasks to
X
,
Y
, and
Z
in parallel to
advanceTimeBy
. So, the tricky thing with this API is the existence of code that runs outside the test dispatchers. I think this issue relates to https://github.com/Kotlin/kotlinx.coroutines/issues/3189, which asks for notifications about the scheduler being idle, but this could be extended to also providing the information about the next pending task when not idle. It seems that, if we decide how to treat the code in other threads concerning idleness, both problems can be solved.