Skip to content

Commit

Permalink
Merge pull request #2941 from brendanmaguire/cached-real-time
Browse files Browse the repository at this point in the history
Add `GenTemporal#cachedRealTime`
  • Loading branch information
djspiewak authored Apr 10, 2022
2 parents becdf26 + 9608fb9 commit f51d87d
Showing 1 changed file with 50 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import cats.data._
import cats.syntax.all._

import scala.concurrent.TimeoutException
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.duration.{Duration, FiniteDuration}

/**
* A typeclass that encodes the notion of suspending fibers for a given duration. Analogous to
Expand Down Expand Up @@ -136,6 +136,55 @@ trait GenTemporal[F[_], E] extends GenConcurrent[F, E] with Clock[F] {
start(f.cancel) *> raiseError[A](ev(new TimeoutException(duration.toString)))
}
}

/**
* Returns a nested effect which returns the time in a much faster way than
* `Clock[F]#realTime`. This is achieved by caching the real time when the outer effect is run
* and, when the inner effect is run, the offset is used in combination with
* `Clock[F]#monotonic` to give an approximation of the real time. The practical benefit of
* this is a reduction in the number of syscalls, since `realTime` will only be sequenced once
* per `ttl` window, and it tends to be (on most platforms) multiple orders of magnitude
* slower than `monotonic`.
*
* This should generally be used in situations where precise "to the millisecond" alignment to
* the system real clock is not needed. In particular, if the system clock is updated (e.g.
* via an NTP sync), the inner effect will not observe that update until up to `ttl`. This is
* an acceptable tradeoff in most practical scenarios, particularly with frequent sequencing
* of the inner effect.
*
* @param ttl
* The period of time after which the cached real time will be refreshed. Note that it will
* only be refreshed upon execution of the nested effect
*/
def cachedRealTime(ttl: Duration): F[F[FiniteDuration]] = {
implicit val self = this

val cacheValuesF = (realTime, monotonic) mapN {
case (realTimeNow, cacheRefreshTime) => (cacheRefreshTime, realTimeNow - cacheRefreshTime)
}

// Take two measurements and keep the one with the minimum offset. This will no longer be
// required when `IO.unyielding` is merged (see #2633)
val minCacheValuesF = (cacheValuesF, cacheValuesF) mapN {
case (cacheValues1 @ (_, offset1), cacheValues2 @ (_, offset2)) =>
if (offset1 < offset2) cacheValues1 else cacheValues2
}

minCacheValuesF.flatMap(ref).map { cacheValuesRef =>
monotonic.flatMap { timeNow =>
cacheValuesRef.get.flatMap {
case (cacheRefreshTime, offset) =>
if (timeNow >= cacheRefreshTime + ttl)
minCacheValuesF.flatMap {
case cacheValues @ (cacheRefreshTime, offset) =>
cacheValuesRef.set(cacheValues).map(_ => cacheRefreshTime + offset)
}
else
pure(timeNow + offset)
}
}
}
}
}

object GenTemporal {
Expand Down

0 comments on commit f51d87d

Please sign in to comment.