common/app/model/Cached.scala (111 lines of code) (raw):

package model import conf.switches.Switches.LongCacheSwitch import conf.switches.Switches.ShorterSurrogateCacheForOlderArticles import conf.switches.Switches.ShorterSurrogateCacheForRecentArticles import org.joda.time.DateTime import play.api.http.Writeable import play.api.mvc._ import scala.math.max import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration.Duration case class CacheTime(cacheSeconds: Int, surrogateSeconds: Option[Int] = None) object CacheTime { // 3800 seems slightly arbitrary, but our CDN caches to disk if above 3700 // https://community.fastly.com/t/why-isnt-serve-stale-working-as-expected/369 private val longCacheTime = 3800 object Default extends CacheTime(60) object LiveBlogActive extends CacheTime(5, Some(60)) def RecentlyUpdated = CacheTime(60, if (ShorterSurrogateCacheForRecentArticles.isSwitchedOn) Some(30) else None) // There is lambda which invalidates the cache on press events, so the facia cache time can be high. object Facia extends CacheTime(60, Some(900)) object Crosswords extends CacheTime(60, Some(900)) object ArchiveRedirect extends CacheTime(60, Some(300)) object ShareCount extends CacheTime(60, Some(600)) object NotFound extends CacheTime(10) // This will be overwritten by fastly object DiscussionDefault extends CacheTime(60) object DiscussionClosed extends CacheTime(60, Some(longCacheTime)) object Football extends CacheTime(10) object FootballMatch extends CacheTime(30) object Cricket extends CacheTime(60) object FootballTables extends CacheTime(60) private def oldArticleCacheTime = if (ShorterSurrogateCacheForOlderArticles.isSwitchedOn) 60 else longCacheTime def LastDayUpdated = CacheTime(60, Some(oldArticleCacheTime)) def NotRecentlyUpdated = CacheTime(60, Some(oldArticleCacheTime)) } object Cached extends implicits.Dates { private val cacheableStatusCodes = Seq(200, 404) private val tenDaysInSeconds = 864000 case class Hash(string: String) sealed trait CacheableResult { def result: Result } case class RevalidatableResult(result: Result, hash: Hash) extends CacheableResult case class WithoutRevalidationResult(result: Result) extends CacheableResult case class PanicReuseExistingResult(result: Result) extends CacheableResult object RevalidatableResult { def apply[C](result: Result, content: C)(implicit writeable: Writeable[C]): RevalidatableResult = { // hashing function from Arrays.java val hashLong: Long = writeable.transform(content).foldLeft(z = 1L) { case (accu, nextByte) => 31 * accu + nextByte } new RevalidatableResult(result, Hash(hashLong.toString)) } def Ok[C](content: C)(implicit writeable: Writeable[C]): RevalidatableResult = { apply(Results.Ok(content), content) } } def apply(seconds: Int)(result: CacheableResult)(implicit request: RequestHeader): Result = { apply(CacheTime(seconds), result, request.headers.get("If-None-Match")) // FIXME could be comma separated } def apply(cacheTime: CacheTime)(result: CacheableResult)(implicit request: RequestHeader): Result = { apply(cacheTime, result, request.headers.get("If-None-Match")) } def apply(duration: Duration)(result: CacheableResult)(implicit request: RequestHeader): Result = { apply(CacheTime(duration.toSeconds.toInt), result, request.headers.get("If-None-Match")) } def apply(page: Page)(revalidatableResult: CacheableResult)(implicit request: RequestHeader): Result = { apply(page.metadata.cacheTime, revalidatableResult, request.headers.get("If-None-Match")) } // Use this when you are sure your result needs caching headers, even though the result status isn't // conventionally cacheable. Typically we only cache 200 and 404 responses. def explicitlyCache(seconds: Int)(result: Result): Result = cacheHeaders(CacheTime(seconds), result, None) def apply(cacheTime: CacheTime, cacheableResult: CacheableResult, ifNoneMatch: Option[String]): Result = { cacheableResult match { case RevalidatableResult(result, hash) if cacheableStatusCodes.contains(result.header.status) => val etag = s"""W/"hash${hash.string}"""" val newResult = if (ifNoneMatch.contains(etag)) Results.NotModified else result cacheHeaders(cacheTime, newResult, Some(etag)) case WithoutRevalidationResult(result) if cacheableStatusCodes.contains(result.header.status) => cacheHeaders(cacheTime, result, None) case PanicReuseExistingResult(result) => cacheHeaders(cacheTime, result, ifNoneMatch) case result: CacheableResult => result.result } } private def cacheControl(maxAge: Int) = { val staleWhileRevalidateSeconds = max(maxAge / 10, 1) s"max-age=$maxAge, stale-while-revalidate=$staleWhileRevalidateSeconds, stale-if-error=$tenDaysInSeconds" } /* NOTE, if you change these headers make sure they are compatible with our Edge Cache see http://tools.ietf.org/html/rfc5861 http://www.fastly.com/blog/stale-while-revalidate http://docs.fastly.com/guides/22966608/40347813 This explains Surrogate-Control vs Cache-Control TLDR Surrogate-Control is used by the CDN, Cache-Control by the browser - do *not* add `private` to Cache-Control https://docs.fastly.com/guides/tutorials/cache-control-tutorial */ private def cacheHeaders(cacheTime: CacheTime, result: Result, maybeEtag: Option[String]): Result = { val now = DateTime.now val surrogateMaxAge = cacheTime.surrogateSeconds match { case Some(age) if LongCacheSwitch.isSwitchedOn => age case _ => cacheTime.cacheSeconds } val etagHeaderString: String = maybeEtag.getOrElse( s""""guRandomEtag${scala.util.Random.nextInt()}${scala.util.Random .nextInt()}"""", // setting a random tag still helps ) result.withHeaders( // the cache headers used by the CDN "Surrogate-Control" -> cacheControl(surrogateMaxAge), // the cache headers that make their way through to the browser "Cache-Control" -> cacheControl(cacheTime.cacheSeconds), "Date" -> now.toHttpDateTimeString, "ETag" -> etagHeaderString, ) } } object NoCache { def apply(result: Result): Result = result.withHeaders("Cache-Control" -> "private, no-store, no-cache") } case class NoCache[A](action: Action[A])(implicit val executionContext: ExecutionContext) extends Action[A] { override def apply(request: Request[A]): Future[Result] = { action(request) map { response => response.withHeaders( ("Cache-Control", "private, no-store, no-cache"), ) } } lazy val parser = action.parser }