the-algorithm/tweetypie/common/src/scala/com/twitter/tweetypie/caching/SoftTtl.scala

121 lines
4.1 KiB
Scala

package com.twitter.tweetypie.caching
import com.twitter.util.Duration
import com.twitter.util.Time
import scala.util.Random
import com.twitter.logging.Logger
/**
* Used to determine whether values successfully retrieved from cache
* are [[CacheResult.Fresh]] or [[CacheResult.Stale]]. This is useful
* in the implementation of a [[ValueSerializer]].
*/
trait SoftTtl[-V] {
/**
* Determines whether a cached value was fresh.
*
* @param cachedAt the time at which the value was cached.
*/
def isFresh(value: V, cachedAt: Time): Boolean
/**
* Wraps the value in Fresh or Stale depending on the value of `isFresh`.
*
* (The type variable U exists because it is not allowed to return
* values of a contravariant type, so we must define a variable that
* is a specific subclass of V. This is worth it because it allows
* us to create polymorphic policies without having to specify the
* type. Another solution would be to make the type invariant, but
* then we would have to specify the type whenever we create an
* instance.)
*/
def toCacheResult[U <: V](value: U, cachedAt: Time): CacheResult[U] =
if (isFresh(value, cachedAt)) CacheResult.Fresh(value) else CacheResult.Stale(value)
}
object SoftTtl {
/**
* Regardless of the inputs, the value will always be considered
* fresh.
*/
object NeverRefresh extends SoftTtl[Any] {
override def isFresh(_unusedValue: Any, _unusedCachedAt: Time): Boolean = true
}
/**
* Trigger refresh based on the length of time that a value has been
* stored in cache, ignoring the value.
*
* @param softTtl Items that were cached longer ago than this value
* will be refreshed when they are accessed.
*
* @param jitter Add nondeterminism to the soft TTL to prevent a
* thundering herd of requests refreshing the value at the same
* time. The time at which the value is considered stale will be
* uniformly spread out over a range of +/- (jitter/2). It is
* valid to set the jitter to zero, which will turn off jittering.
*
* @param logger If non-null, use this logger rather than one based
* on the class name. This logger is only used for trace-level
* logging.
*/
case class ByAge[V](
softTtl: Duration,
jitter: Duration,
specificLogger: Logger = null,
rng: Random = Random)
extends SoftTtl[Any] {
private[this] val logger: Logger =
if (specificLogger == null) Logger(getClass) else specificLogger
private[this] val maxJitterMs: Long = jitter.inMilliseconds
// this requirement is due to using Random.nextInt to choose the
// jitter, but it allows jitter of greater than 24 days
require(maxJitterMs <= (Int.MaxValue / 2))
// Negative jitter probably indicates misuse of the API
require(maxJitterMs >= 0)
// we want period +/- jitter, but the random generator
// generates non-negative numbers, so we generate [0, 2 *
// maxJitter) and subtract maxJitter to obtain [-maxJitter,
// maxJitter)
private[this] val maxJitterRangeMs: Int = (maxJitterMs * 2).toInt
// We perform all calculations in milliseconds, so convert the
// period to milliseconds out here.
private[this] val softTtlMs: Long = softTtl.inMilliseconds
// If the value is below this age, it will always be fresh,
// regardless of jitter.
private[this] val alwaysFreshAgeMs: Long = softTtlMs - maxJitterMs
// If the value is above this age, it will always be stale,
// regardless of jitter.
private[this] val alwaysStaleAgeMs: Long = softTtlMs + maxJitterMs
override def isFresh(value: Any, cachedAt: Time): Boolean = {
val ageMs: Long = (Time.now - cachedAt).inMilliseconds
val fresh =
if (ageMs <= alwaysFreshAgeMs) {
true
} else if (ageMs > alwaysStaleAgeMs) {
false
} else {
val jitterMs: Long = rng.nextInt(maxJitterRangeMs) - maxJitterMs
ageMs <= softTtlMs + jitterMs
}
logger.ifTrace(
s"Checked soft ttl: fresh = $fresh, " +
s"soft_ttl_ms = $softTtlMs, age_ms = $ageMs, value = $value")
fresh
}
}
}