Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump ehcache to version 3 #12526

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.sbt
Expand Up @@ -294,7 +294,8 @@ lazy val PlayAhcWsProject = PlayCrossBuiltProject("Play-AHC-WS", "transport/clie
// quieten deprecation warnings in tests
(Test / scalacOptions) := (Test / scalacOptions).value.diff(Seq("-deprecation"))
)
.dependsOn(PlayWsProject, PlayCaffeineCacheProject % "test")
.dependsOn(PlayWsProject)
.dependsOn(PlayCaffeineCacheProject % "test")
.dependsOn(PlaySpecs2Project % "test")
.dependsOn(PlayTestProject % "test->test")
.dependsOn(PlayPekkoHttpServerProject % "test") // Because we need a server provider when running the tests
Expand Down
Expand Up @@ -2,11 +2,11 @@
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.cache.caffeine
package play.api.cache

import scala.concurrent.duration.Duration

import org.apache.pekko.annotation.InternalApi

@InternalApi
private[caffeine] case class ExpirableCacheValue[V](value: V, durationMaybe: Option[Duration] = None)
case class ExpirableCacheValue[V](value: V, durationMaybe: Option[Duration] = None)
Expand Up @@ -9,6 +9,7 @@ import scala.concurrent.duration.Duration

import com.github.benmanes.caffeine.cache.Expiry
import org.apache.pekko.annotation.InternalApi
import play.api.cache.ExpirableCacheValue

@InternalApi
private[caffeine] class DefaultCaffeineExpiry extends Expiry[String, ExpirableCacheValue[Any]] {
Expand Down
Expand Up @@ -4,7 +4,7 @@

package play.cache.ehcache;

import net.sf.ehcache.CacheManager;
import org.ehcache.CacheManager;
import play.Environment;
import play.api.cache.ehcache.CacheManagerProvider;
import play.api.cache.ehcache.EhCacheApi;
Expand Down
29 changes: 15 additions & 14 deletions cache/play-ehcache/src/main/resources/ehcache-default.xml
Expand Up @@ -2,18 +2,19 @@
Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
-->

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd" updateCheck="false">
<config
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns='http://www.ehcache.org/v3'
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd">

<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
maxElementsOnDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>

</ehcache>
<cache-template name="default">
<key-type>java.lang.String</key-type>
<value-type>play.api.cache.ehcache.EhCacheApi.CacheValue</value-type>
<expiry>
<ttl unit="seconds">120</ttl>
</expiry>
<resources>
<heap unit="entries">10000</heap>
</resources>
</cache-template>
</config>
Expand Up @@ -4,25 +4,35 @@

package play.api.cache.ehcache

import java.time
import java.util.function.Supplier
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton

import scala.concurrent.duration
import scala.concurrent.duration.Duration
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.reflect.ClassTag

import com.google.common.primitives.Primitives
import net.sf.ehcache.CacheManager
import net.sf.ehcache.Ehcache
import net.sf.ehcache.Element
import net.sf.ehcache.ObjectExistsException
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.annotation.InternalApi
import org.apache.pekko.stream.Materializer
import org.apache.pekko.Done
import org.ehcache.config.builders.CacheConfigurationBuilder
import org.ehcache.config.builders.CacheManagerBuilder
import org.ehcache.config.builders.ResourcePoolsBuilder
import org.ehcache.expiry.ExpiryPolicy
import org.ehcache.xml.XmlConfiguration
import org.ehcache.Cache
import org.ehcache.CacheManager
import play.api.cache._
import play.api.cache.ehcache.EhCacheApi.EhExpirableCacheValue
import play.api.cache.ehcache.EhCacheApi.PlayEhCache
import play.api.cache.ExpirableCacheValue
import play.api.inject._
import play.api.Configuration
import play.api.Environment
Expand Down Expand Up @@ -85,7 +95,7 @@ class EhCacheModule
// bind a cache with the given name
def bindCache(name: String) = {
val namedCache = named(name)
val ehcacheKey = bind[Ehcache].qualifiedWith(namedCache)
val ehcacheKey = bind[PlayEhCache].qualifiedWith(namedCache)
val cacheApiKey = bind[AsyncCacheApi].qualifiedWith(namedCache)
Seq(
ehcacheKey.to(new NamedEhCacheProvider(name, createBoundCaches)),
Expand Down Expand Up @@ -113,26 +123,64 @@ class CacheManagerProvider @Inject() (env: Environment, config: Configuration, l
lazy val get: CacheManager = {
val resourceName = config.underlying.getString("play.cache.configResource")
val configResource = env.resource(resourceName).getOrElse(env.classLoader.getResource("ehcache-default.xml"))
val manager = CacheManager.create(configResource)
lifecycle.addStopHook(() => Future.successful(manager.shutdown()))
val configuration = new XmlConfiguration(configResource)
val manager = CacheManagerBuilder.newCacheManager(configuration)
manager.init()
lifecycle.addStopHook(() => Future.successful(manager.close()))
manager
}
}

private[play] class NamedEhCacheProvider(name: String, create: Boolean) extends Provider[Ehcache] {
private[play] class NamedEhCacheProvider(name: String, create: Boolean) extends Provider[PlayEhCache] {
@Inject private var manager: CacheManager = _
lazy val get: Ehcache = NamedEhCacheProvider.getNamedCache(name, manager, create)
lazy val get: PlayEhCache = NamedEhCacheProvider.getNamedCache(name, manager, create)
}

private[play] object NamedEhCacheProvider {
def getNamedCache(name: String, manager: CacheManager, create: Boolean): Ehcache =

private val expiryPolicy = new ExpiryPolicy[String, EhExpirableCacheValue]() {
def getExpiryForCreation(key: String, value: EhExpirableCacheValue): time.Duration = value.durationMaybe match {
case Some(finite: FiniteDuration) =>
val seconds = finite.toSeconds
if (seconds <= 0) {
time.Duration.ZERO
} else if (seconds > Int.MaxValue) {
ExpiryPolicy.INFINITE
} else {
time.Duration.ofSeconds(seconds.toInt)
}
case _ => ExpiryPolicy.INFINITE
}

def getExpiryForAccess(key: String, value: Supplier[? <: EhExpirableCacheValue]): time.Duration = null

def getExpiryForUpdate(
key: String,
oldValue: Supplier[? <: EhExpirableCacheValue],
newValue: EhExpirableCacheValue
): time.Duration = null
}

private def cacheConfigurationBuilder(manager: CacheManager) = {
val builder = manager.getRuntimeConfiguration match {
case configuration: XmlConfiguration =>
configuration
.newCacheConfigurationBuilderFromTemplate("default", classOf[String], classOf[EhExpirableCacheValue])
case _ =>
CacheConfigurationBuilder
.newCacheConfigurationBuilder(classOf[String], classOf[EhExpirableCacheValue], ResourcePoolsBuilder.heap(100))
}
builder.withExpiry(expiryPolicy)
}

def getNamedCache(name: String, manager: CacheManager, create: Boolean): PlayEhCache =
try {
if (create) {
manager.addCache(name)
manager.createCache(name, cacheConfigurationBuilder(manager))
}
manager.getEhcache(name)
manager.getCache(name, classOf[String], classOf[EhExpirableCacheValue])
} catch {
case e: ObjectExistsException =>
case e: IllegalArgumentException =>
throw EhCacheExistsException(
s"""An EhCache instance with name '$name' already exists.
|
Expand All @@ -143,7 +191,7 @@ private[play] object NamedEhCacheProvider {
}
}

private[play] class NamedAsyncCacheApiProvider(key: BindingKey[Ehcache]) extends Provider[AsyncCacheApi] {
private[play] class NamedAsyncCacheApiProvider(key: BindingKey[PlayEhCache]) extends Provider[AsyncCacheApi] {
@Inject private var injector: Injector = _
@Inject private var defaultEc: ExecutionContext = _
@Inject private var config: Configuration = _
Expand Down Expand Up @@ -185,23 +233,10 @@ private[play] class NamedCachedProvider(key: BindingKey[AsyncCacheApi]) extends

private[play] case class EhCacheExistsException(msg: String, cause: Throwable) extends RuntimeException(msg, cause)

class SyncEhCacheApi @Inject() (private[ehcache] val cache: Ehcache) extends SyncCacheApi {
class SyncEhCacheApi @Inject() (private[ehcache] val cache: PlayEhCache) extends SyncCacheApi {
override def set(key: String, value: Any, expiration: Duration): Unit = {
val element = new Element(key, value)
expiration match {
case infinite: Duration.Infinite => element.setEternal(true)
case finite: FiniteDuration =>
val seconds = finite.toSeconds
if (seconds <= 0) {
element.setTimeToLive(1)
} else if (seconds > Int.MaxValue) {
element.setTimeToLive(Int.MaxValue)
} else {
element.setTimeToLive(seconds.toInt)
}
}
cache.put(element)
Done
cache.put(key, ExpirableCacheValue[Any](value, Some(expiration)))
()
}

override def remove(key: String): Unit = cache.remove(key)
Expand All @@ -218,7 +253,7 @@ class SyncEhCacheApi @Inject() (private[ehcache] val cache: Ehcache) extends Syn

override def get[T](key: String)(implicit ct: ClassTag[T]): Option[T] = {
Option(cache.get(key))
.map(_.getObjectValue)
.map(_.value)
.filter { v =>
Primitives.wrap(ct.runtimeClass).isInstance(v) ||
ct == ClassTag.Nothing || (ct == ClassTag.Unit && v == ((): Unit).asInstanceOf[Any])
Expand All @@ -230,7 +265,7 @@ class SyncEhCacheApi @Inject() (private[ehcache] val cache: Ehcache) extends Syn
/**
* Ehcache implementation of [[AsyncCacheApi]]. Since Ehcache is synchronous by default, this uses [[SyncEhCacheApi]].
*/
class EhCacheApi @Inject() (private[ehcache] val cache: Ehcache)(implicit context: ExecutionContext)
class EhCacheApi @Inject() (private[ehcache] val cache: PlayEhCache)(implicit context: ExecutionContext)
extends AsyncCacheApi {
override lazy val sync: SyncEhCacheApi = new SyncEhCacheApi(cache)

Expand All @@ -256,7 +291,12 @@ class EhCacheApi @Inject() (private[ehcache] val cache: Ehcache)(implicit contex
}

def removeAll(): Future[Done] = Future {
cache.removeAll()
cache.clear()
Done
}
}

object EhCacheApi {
type EhExpirableCacheValue = ExpirableCacheValue[Any]
type PlayEhCache = Cache[String, EhExpirableCacheValue]
}
72 changes: 58 additions & 14 deletions cache/play-ehcache/src/test/scala/play/api/cache/CachedSpec.scala
Expand Up @@ -4,18 +4,35 @@

package play.api.cache

import java.nio.file.Files
import java.time.{ Duration => JDurarion }
import java.time.Instant
import java.util.concurrent.atomic.AtomicInteger
import javax.inject._

import scala.concurrent.duration._
import scala.concurrent.Future
import scala.util.Random

import org.ehcache.config.builders.CacheConfigurationBuilder
import org.ehcache.config.builders.CacheManagerBuilder
import org.ehcache.config.builders.ConfigurationBuilder
import org.ehcache.config.builders.ExpiryPolicyBuilder
import org.ehcache.config.builders.ResourcePoolsBuilder
import org.ehcache.config.units.MemoryUnit
import org.ehcache.impl.config.persistence.DefaultPersistenceConfiguration
import org.ehcache.CacheManager
import play.api.cache.ehcache.EhCacheApi
import play.api.cache.ehcache.EhCacheApi.EhExpirableCacheValue
import play.api.cache.ehcache.EhCacheApi.PlayEhCache
import play.api.http
import play.api.inject
import play.api.inject.ApplicationLifecycle
import play.api.mvc._
import play.api.test._
import play.api.Application
import play.api.Configuration
import play.api.Environment

class CachedSpec extends PlaySpecification {
sequential
Expand Down Expand Up @@ -66,24 +83,31 @@ class CachedSpec extends PlaySpecification {
}
}

"cache values to disk using injected CachedApi" in new WithApplication() {
"cache values to disk using injected CachedApi" in new WithApplication(
// default implementation does not do disk caching, so inject a custom one
_.overrides(inject.bind[CacheManager].toProvider[PersistentCacheManagerProvider])
) {
override def running() = {
import net.sf.ehcache._
import net.sf.ehcache.config._
import net.sf.ehcache.store.MemoryStoreEvictionPolicy
import org.ehcache._
// FIXME: Do this properly
val cacheManager = app.injector.instanceOf[CacheManager]
val diskEhcache = new Cache(
new CacheConfiguration("disk", 30)
.memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU)
.eternal(false)
.timeToLiveSeconds(60)
.timeToIdleSeconds(30)
.diskExpiryThreadIntervalSeconds(0)
.persistence(new PersistenceConfiguration().strategy(PersistenceConfiguration.Strategy.LOCALTEMPSWAP))

cacheManager.createCache(
"disk",
CacheConfigurationBuilder
.newCacheConfigurationBuilder(
classOf[String],
classOf[EhExpirableCacheValue],
ResourcePoolsBuilder
.newResourcePoolsBuilder()
.disk(1, MemoryUnit.MB)
)
.withExpiry(
ExpiryPolicyBuilder.timeToIdleExpiration(JDurarion.ofSeconds(30))
)
)
cacheManager.addCache(diskEhcache)
val diskEhcache2 = cacheManager.getCache("disk")

val diskEhcache2: PlayEhCache = cacheManager.getCache("disk", classOf[String], classOf[EhExpirableCacheValue])
assert(diskEhcache2 != null)
val diskCache = new EhCacheApi(diskEhcache2)(app.materializer.executionContext)
val diskCached = new Cached(diskCache)
Expand Down Expand Up @@ -399,3 +423,23 @@ class NamedCachedController @Inject() (
val action = cached(_ => "foo")(Action(Results.Ok("" + invoked.incrementAndGet())))
def isCached(key: String): Boolean = cache.sync.get[String](key).isDefined
}

class PersistentCacheManagerProvider @Inject() (
env: Environment,
config: Configuration,
lifecycle: ApplicationLifecycle
) extends Provider[CacheManager] {

lazy val tempDir = Files.createTempDirectory("cache").toFile()

lazy val get: CacheManager = {
val configuration = ConfigurationBuilder
.newConfigurationBuilder()
.withService(new DefaultPersistenceConfiguration(tempDir))
.build()
val manager = CacheManagerBuilder.newCacheManager(configuration)
manager.init()
lifecycle.addStopHook(() => Future.successful(manager.close()))
manager
}
}