-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Added Caffeine Cache module and made it the default Cache #8025
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for taking care of this, @dusanstanojeviccs. Some observations:
- We need to provide a clear migration path to users, so this change needs to be explained in https://github.com/playframework/playframework/blob/master/documentation/manual/releases/release27/migration27/Migration27.md. Ideally, we should tell users how to use EhCache if they don't want (or can't) migrate to Caffeine.
- It could be good to have a small page showing how to use EhCache if users want to use it instead.
|
||
## JCache Support | ||
|
||
Ehcache implements the [JSR 107](https://github.com/jsr107/jsr107spec) specification, also known as JCache, but Play does not bind `javax.caching.CacheManager` by default. To bind `javax.caching.CacheManager` to the default provider, add the following to your dependencies list: | ||
Caffeine Cache implements the [JSR 107](https://github.com/jsr107/jsr107spec) specification, also known as JCache, but Play does not bind `javax.caching.CacheManager` by default. To bind `javax.caching.CacheManager` to the default provider, add the following to your dependencies list: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ehcache is a native JCache provider, since they were the main authors of the spec.
Caffeine isn't, since I consider the spec to be pretty bad. Instead it provides an optional implementation as an additional dependency. So you may need to clarify the additional caffeine-jcache dependency is required, too. Or you could consider it self-evident since jcache configuration is mostly non-standard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, changing the docs now will make a commit with more information and a reference to caffeine-jcache dependency.
@@ -55,7 +57,7 @@ You can also supply a `Callable` that generates stores the value if no value is | |||
|
|||
@[get-or-else](code/javaguide/cache/JavaCache.java) | |||
|
|||
**Note**: `getOrElseUpdate` is not an atomic operation in Ehcache and is implemented as a `get` followed by computing the value from the `Callable`, then a `set`. This means it's possible for the value to be computed multiple times if multiple threads are calling `getOrElse` simultaneously. | |||
**Note**: `getOrElseUpdate` is not an atomic operation in Caffeine Cache and is implemented as a `get` followed by computing the value from the `Callable`, then a `set`. This means it's possible for the value to be computed multiple times if multiple threads are calling `getOrElse` simultaneously. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an unfortunate aspect of the Play! api. Caffeine does offer atomic operations, but not with a custom expiration passed through. Instead the Expiry
is used to evaluate, since often the calls should prefer to use a LoadingCache
rather than a cache.get(key, func)
. This is the same problem with Ehcache and others, where that Play! api doesn't map neatly.
@gmethvin Is the lack of memoization a negative for Play! users? If so, you might want to implement this generically using lock striping for all providers to take advantage of. That's merely Lock lock = locks[hash(key) mod locks.length]
, etc. There some minor optimization tricks, but would note.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understood that correctly get and set are not atomic if expiration is set and we should do locking at the integration level?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in that we don't provide a computeIfAbsent(key, func, expiration)
type of call like Play! does. Neither does Ehcache, so both are using get-compute-put. There are atomic computeIfAbsent(key, func)
type methods, but they call back into the Expiry to get the information. You would have to do some nasty thread-local to hack it where the TL is set, the Expiry queries it, and the TL is unset. I doubt that's really desirable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh got it, so it would stay the same, right? Before there was just a note in the docs that the method was doing get-compute-put and everyone was ok with it, so I guess it would be ok if that stayed the same since Play! users would expect it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, its just a quirk of Play's APIs rather than a missing feature. That API method does not map neatly to any caching library, and isn't worth providing ad hoc just for Play when it is fairly ugly (when compared to the preferred). Since this is an issue across all providers, the best fix might be to add the feature into Play's abstraction. Using lock striping is cheap and simple code, so a suggestion for @gmethvin to consider someday.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is an unfortunate design quirk of the Play cache. There has also been discussion of allowing a configurable default for the methods when an explicit expiration is not passed. That would probably make more sense than defaulting to infinite.
@dusanstanojeviccs I think for the purposes of this PR, the goal should be parity with the current ehcache-based implementation, so this disclaimer would apply to both caffeine and ehcache. If you have interest in implementing lock striping to solve the problem as @ben-manes described, that would be a nice addition, but it could also be done in a future PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I am only putting this on @gmethvin radar and not asking for it in this PR. Ignore this distraction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can probably do this and I like the locks[hash(key) mod locks.length], but what should the size of locks be, should it be scaled up and down depending on the number of keys currently in the cache (like an arraylist) in sync(locks) blocks or should it just have a constant size set up front in the config file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be in a subsequent PR.
Generally you size ad hoc, since any decent number will offer enough concurrency. If you try to be fancy, you can base it on Runtime.getRuntime().availableProcessors()
but it probably won't help. A setting like 2048 is plentiful and perhaps larger than necessary. Generally you want to minimize collisions but since writes are rarer, the likelihood of overlap isn't high. You would use a power-of-two in order to use the cheaper modulus trick of N & (N - 1)
. There always reaches a point where increasing the number of stripes stops being beneficial and a redesign is required instead.
The stripe can be an immutable Object[] array with unique instances. Then one would use double-checked locking to avoid synchronizing in the happy-path of the entry being present. This way calls for the same key will collide and be blocked, with a high probability for distinct keys compute concurrently.
V value = cache.getIfPresent(key);
if (value != null) {
return value;
}
Object lock = locks[hash(key) & MASK];
synchronized (lock) {
value = cache.getIfPresent(key);
if (value == null) {
value = // compute
cache.put(key, value, expiration);
}
return value;
}
A supplementary hash function, like murmur3 or wang-jenkins, would help the distribution if key.hashCode()
was of poor quality (which I think String is). For example the spreader in the original ConcurrentHashMap
(based on fixed stripes vs dynamic tree-bins in JDK8) was
private static int hash(int h) {
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
I would imagine a striped CacheApi would be a trait or decorator for reuse - whatever is idiomatic these days. The underlying cache would assumed to be concurrent, so only set
calls would need to use the lock to compute with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ben-manes Sorry I have not responded sooner, I was traveling and then had to catch up on work. Wow! That is beautiful and makes total sense, the whole N & (N - 1) is brilliant! I will definitely do this in a future PR. Thank you for the pointers.
|
||
play.cache.createBoundCaches = false | ||
play.cache.caffeine.spec = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you changed from the spec string to the parser?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did, but I named the configuration value 'play.cache.caffeine.spec'. Would it be clearer for Caffeine users if I changed it to 'play.cache.caffeine.parser'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, nevermind then. I thought this was the unparsed spec string. It might be good to see an example of how a named cache works (just here for me to grok it). I'd have thought it would be play.cache.caffeine.<name>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've looked at the current ehcache.xml example and it looked like it was never bound to a cache name so I figured that was not part of the spec before. But to be honest that makes a lot of sense. Doing it now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, running tests now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not drop the spec
?
play.cache.caffeine {
play {
maximum-size: 200
}
custom {
...
}
}
The Caffeine JCache adapter uses this type of configuration. but I don't know if it is considered idiomatic. Hopefully the core devs here have an opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No good reason to keep the spec so I dropped it. I have not seen any modules that do this so I am not sure if it is idiomatic either. I would imagine it would be fine as long as it got documented in the official docs but let's hold off on it until we get an opinion from one of the core devs. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about something like:
play.cache.caffeine {
defaults {
maximumSize = 200
...
}
caches {
play = {}
custom-cache = {}
}
}
This is similar to what we do on database configuration: https://github.com/playframework/playframework/blob/master/framework/src/play-jdbc/src/main/resources/reference.conf#L28.
We can also include the reference.conf directly in the page like we do on other pages, e.g. https://github.com/playframework/playframework/blob/2.6.x/documentation/manual/working/commonGuide/configuration/SettingsAkkaHttp.md
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gmethvin Looks good, I've added the code for this in my last commit. So to be clear defaults is used if "play.cache.caffeine.caches" does not contain the cache name as a child.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The configuration for each cache would be layered on top of the default. The presence of the play
key would indicate that a cache called play
should be created, and the values in that object would override the default values with the same name.
private void parse(String key) { | ||
switch (key) { | ||
case "initial-capacity": | ||
System.out.println("initial-capacity:" + config.getInt(key)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does Play! not have a logger?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does, I've put this for checking if everything works, I just personally like using System.out because I see no advantage of logger when testing... I've removed it now.
System.out.println("maximum-weight:" + config.getMemorySize(key)); | ||
cacheBuilder.maximumWeight(config.getMemorySize(key).toBytes()); | ||
break; | ||
case "expire-after-access": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since variable expiration is required for the play API, these fixed versions cannot be used simultaneously. So you should remove.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, I've removed: expire-after-access, expire-after-write, refreshAfterWrite.
val seconds = finite.toSeconds | ||
if (seconds <= 0) { | ||
cache.policy().expireVariably().get().put(key, value, 1, TimeUnit.SECONDS) | ||
} else if (seconds > Int.MaxValue) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the duration
is a long
parameter, you can probably drop this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, because this was only needed before because the expiration was int, now it's pointless, good catch.
What is Play's approach to exporting statistics? Ehcache does it using JMX, but it is more framework-oriented than Caffeine. For us, we expose stats as pull ( |
@dusanstanojeviccs Haven't had a chance to fully review, but this should be another module, in addition to the existing ehcache module. It should be no problem to have both modules documented and available to use. |
@gmethvin Got it, I think I removed it when I started working on this because I did not have a good grasp of how the whole Play build system with sbt is organized, but I have brought it back and all 'sbt tests' have passed :) I have addressed the coding issues. Now I am working on improving the docs for Play 2.7 migration and adding/correcting a few things about caffeine and jcache. Hopefully, the docs will be done tonight. |
1. Removed testing loggers 2. removed not needed code 3. added support for named cache configs
Caffeine cacheBuilder; | ||
|
||
// we create the cache builder | ||
// depending on weather the config for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whether
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup
@marcospereira Loved the doc suggestions. I will do a few more doc changes tonight, but I think I'll do both of your requested changes tomorrow. |
framework/build.sbt
Outdated
).dependsOn(PlayWsProject, PlayEhcacheProject % "test") | ||
.dependsOn(PlaySpecs2Project % "test") | ||
.dependsOn(PlayTestProject % "test->test") | ||
).dependsOn(PlayWsProject, PlayCaffeineCacheProject) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be a compile dependency rather than just a test dependency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it was test before it should be test now since I did not change the Ahc module in any way, fixed and committed
|
||
if (config.hasPath(pathKey)) { | ||
cacheBuilder = CaffeineParser.from(config.getConfig(pathKey)).expireAfter(defaultExpiry); | ||
} else if (config.hasPath(PLAY_CACHE_CAFFEINE_DEFAULTS)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't need to check hasPath
if it's in reference.conf. Basically the config is:
val configDefaults = config.getConfig(PLAY_CACHE_CAFFEINE_DEFAULTS)
config.getConfig(pathKey).withFallback(configDefaults)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
YES! I did not know withFallback existed, it made the code so much simpler. Thank you!
} | ||
|
||
@SuppressWarnings("fallthrough") | ||
private void parse(String key) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer if we explicitly got the configuration elements we need.
These should all be documented in reference.conf
. Ideally our defaults
should look something like:
defaults = {
initial-capacity = null
maximum-size = null
maximum-weight = null
weak-keys = false
soft-values = false
record-stats = false
}
In this case null
can mean to use caffeine's default value. If Play wants to override a value by default, then it's as easy as updating the reference.conf
.
Then we can explicitly get each of the keys. I don't really mind if you write this in Java but it would probably be simpler to write it in Scala using the play Configuration
helper:
configuration.get[Option[Int]]("initial-capacity") foreach cacheBuilder.initialCapacity
configuration.get[Option[Long]]("maximum-size") foreach cacheBuilder.maximumSize
if (configuration.get[Boolean]("soft-values")) cacheBuilder.softValues()
and so on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, fallthrough has been implemented, I've kept the file in Java and just put 3 if statements around it for null to not set the values. It should be pretty easy to understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maximum-weight requires a Weigher, but I don’t think there is a current way to pass in an instance. A class literal with a Guice injector query would be a sad solution, though. So I don’t think this feature can be exposed without some additional work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, so I've removed maximum-weight, is that the right thing to do for now? If the feature becomes exposed we can always add it to a future PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I buy that fallthrough is the right way to do this. Even if you don't want to use the scala Configuration
wrapper, you can use:
private void nullable(String key, Consumer<String> ifNotNull) {
if (!config.getIsNull(key)) {
ifNotNull(key);
}
}
then getting nullable keys is as easy as:
nullable("initial-capacity", key -> cacheBuilder.initialCapacity(config.getInt(key)))
nullable("maximum-size", key -> cacheBuilder.maximumSize(config.getLong(key)))
If we have default values in reference.conf
we don't need to worry about the case where the key does not exist.
And I don't think this code here works for null values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might not be understanding what you mean. Nulls are not connected to the fallthrough and are handled on line 64 in CaffeineParser, since there are 2 places they are called the whole nullable method seems like an overkill as compared to 2 if statements. I might be wrong but let me know if I misunderstood something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably doesn't help, but I use fall-through and a default template in a similar way in my JCache adapter. It has to deal with some merging logic, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I think you made the changes to handle nulls after I commented here. Still, I don't understand why the switch
statement is necessary for this at all. Seems like overkill to have extra code to iterate over a map looking at keys, then other code to again get those keys. If you know what keys you're looking for you can just get them right away, since if we have default values in reference.conf
we don't need to worry about them not existing.
* CaffeineCache components for compile time injection | ||
*/ | ||
trait CaffeineCacheComponents { | ||
def environment: Environment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't look like you need environment
or applicationLifecycle
in here at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, it's removed :)
/** | ||
* Use this to create with the given name. | ||
*/ | ||
def cacheApi(name: String, create: Boolean = true): AsyncCacheApi = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the create
needed here? We don't need to worry about having the exact same API as EhCacheComponents
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not used and since we dont have to keep the same API it is removed
*/ | ||
package play.cache.caffeine; | ||
|
||
public class CaffeineConfigConstants { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading your code I actually don't find it useful to have these in global string constants. Generally most of these need to be referenced in exactly one place besides the configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with you on this one, I was planning to remove them. I usually don't like having string literals in the code but they seem to be better for readability when it comes to config keys...
new CaffeineCacheApi(NamedCaffeineCacheProvider.getNamedCache(name, caffeineCacheManager, configuration))(ec) | ||
} | ||
|
||
lazy val defaultCacheApi: AsyncCacheApi = cacheApi(PLAY_CACHE_CAFFEINE_DEFAULT_NAME) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should use the default cache name, i.e. the value of the play.cache.defaultCache
configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is a good catch, interestingly this was always passing "play" in the EhCache implementation
* } | ||
* </pre> | ||
*/ | ||
public interface CaffeineCacheComponents extends ConfigurationComponents, AkkaComponents { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comments as in the scala components.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both are done
I think so. There are a few similar features, like removal listener, that
could go into a future PR when users actually request them. Minimal seems
wise for now.
…On Tue, Dec 5, 2017 at 10:31 PM dusanstanojeviccs ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In
framework/src/play-caffeine-cache/src/main/java/play/cache/caffeine/CaffeineParser.java
<#8025 (comment)>
:
> + private CaffeineParser(Config config) {
+ this.cacheBuilder = Caffeine.newBuilder();
+ this.config = Objects.requireNonNull(config);
+ }
+
+ /** Returns a configured ***@***.*** Caffeine} cache builder. */
+ public static Caffeine<Object, Object> from(Config config) {
+ CaffeineParser parser = new CaffeineParser(config);
+
+ config.entrySet().stream().map(Map.Entry::getKey).forEach(parser::parse);
+
+ return parser.cacheBuilder;
+ }
+
+ @SuppressWarnings("fallthrough")
+ private void parse(String key) {
Got it, so I've removed maximum-weight, is that the right thing to do for
now? If the feature becomes exposed we can always add it to a future PR.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#8025 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAXG9oeed-OEucbBjYCk0kx4I7Z4Grs0ks5s9jTJgaJpZM4Qlawz>
.
|
} | ||
|
||
@SuppressWarnings("fallthrough") | ||
private void parse(String key) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I buy that fallthrough is the right way to do this. Even if you don't want to use the scala Configuration
wrapper, you can use:
private void nullable(String key, Consumer<String> ifNotNull) {
if (!config.getIsNull(key)) {
ifNotNull(key);
}
}
then getting nullable keys is as easy as:
nullable("initial-capacity", key -> cacheBuilder.initialCapacity(config.getInt(key)))
nullable("maximum-size", key -> cacheBuilder.maximumSize(config.getLong(key)))
If we have default values in reference.conf
we don't need to worry about the case where the key does not exist.
And I don't think this code here works for null values.
cache { | ||
# Data that should be used to configure the cache | ||
caffeine { | ||
defaults {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add the default values to reference.conf? It should look something like:
defaults {
initialCapacity = null
maximumSize = null
weakKeys = false
softValues = false
recordStats = false
}
Even if the value is null
meaning the caffeine default, it's useful to have the settings documented here.
I was also thinking we should use camelCase here since that's consistent with what caffeine uses and camelCase is used in other places in this configuration. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the HOCON spec,
Config keys are encouraged to be hyphen-separated rather than camelCase.
https://github.com/lightbend/config/blob/master/HOCON.md#hyphen-separated-vs-camelcase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this with hyphen separated key names:
caffeine {
defaults {
initial-capacity = null
weak-keys = null
weak-keys = false
soft-values = false
record-stats = false
}
caches {}
}
For stats, I think the most basic implementation would be for the user to inject the cache instance and call |
@@ -7,32 +7,51 @@ Caching data is a typical optimization in modern applications, and so Play provi | |||
|
|||
For any data stored in the cache, a regeneration strategy needs to be put in place in case the data goes missing. This philosophy is one of the fundamentals behind Play, and is different from Java EE, where the session is expected to retain values throughout its lifetime. | |||
|
|||
The default implementation of the Cache API uses [Ehcache](http://ehcache.org/). | |||
Implmentations of both [Caffeine](https://github.com/ben-manes/caffeine/) and [EhCache](http://www.ehcache.org) are provided, using the Caffeine implementation is recommended. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in the first word: Implementation
To add only the API, add `cacheApi` to your dependencies list. | ||
|
||
@[cache-sbt-dependencies](code/cache.sbt) | ||
|
||
The API dependency is useful if you'd like to define your own bindings for the `Cached` helper and `AsyncCacheApi`, etc., without having to depend on Ehcache. If you're writing a custom cache module you should use this. | ||
The API dependency is useful if you'd like to define your own bindings for the `Cached` helper and `AsyncCacheApi`, etc., without having to depend on Caffeine Cache. If you're writing a custom cache module you should use this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of "caffeine cache" I'd just say "any specific cache impementation"
|
||
For any data stored in the cache, a regeneration strategy needs to be put in place in case the data goes missing. This philosophy is one of the fundamentals behind Play, and is different from Java EE, where the session is expected to retain values throughout its lifetime. | ||
|
||
The default implementation of the cache API uses [Ehcache](http://www.ehcache.org/). | ||
Implmentations of both [Caffeine](https://github.com/ben-manes/caffeine/) and [EhCache](http://www.ehcache.org) are provided, using the Caffeine implementation is recommended. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can change the wording slightly here to suggest that the Ehcache 2.x implementation is a "legacy" implementation. Something like this:
"Play provides a CacheApi
implementation based on Caffeine and a legacy implementation based on Ehcache 2.x. For in-process caching Caffeine is typically the best choice. If you need distributed caching, there are third-party plugins for memcached and redis."
It seems useful to mention the redis and memcached plugins because Play doesn't provide any guidance on how to do distributed caching with ehcache, and it usually makes more sense to use something else.
/cc @marcospereira: I'm assuming the plan is to introduce the new caffeine implementation and later deprecate and remove the ehcache impl completely.
Play provides both an API and an default Ehcache implementation of that API. To get the full Ehcache implementation, add `ehcache` to your dependencies list: | ||
### Caffeine | ||
|
||
Play provides both the API and the default Caffeine implementation of that API. To get the Caffeine implementation, add `caffeine` to your dependencies list: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first sentence here "Play provides both the API and ..." (in both sections) should be moved to the top of the "Importing the Cache API" section and be something like "Play provides separate dependencies for the Cache API and for the Caffeine and Ehcache implementations."
framework/project/Dependencies.scala
Outdated
val playAhcWsDeps = Seq( | ||
"com.typesafe.play" %% "play-ahc-ws-standalone" % playWsStandaloneVersion, | ||
"com.typesafe.play" % "shaded-asynchttpclient" % playWsStandaloneVersion, | ||
"com.typesafe.play" % "shaded-oauth" % playWsStandaloneVersion, | ||
"com.github.ben-manes.caffeine" % "jcache" % caffeineVersion % Test | ||
"com.github.ben-manes.caffeine" % "jcache" % caffeineVersion % Test, | ||
"net.sf.ehcache" % "ehcache" % ehcacheVersion, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you need to add the ehcache dependency here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, thanks for pointing this out, the file OptionalAhcHttpCacheProviderSpec.scala has a test written for using ehcache with jcache, I've mirrored this into one more test like this:
val settings = Map(
"play.ws.cache.enabled" -> "true",
"play.ws.cache.cachingProviderName" -> classOf[CaffeineCachingProvider].getName,
"play.ws.cache.cacheManagerResource" -> "caffeine.conf"
)
So the whole ahc is now dependent on both caffeine and ehcache for this test to run. I am not sure if we want to change the ahc in this PR, if we want to do that let me know in which way and I'll adjust it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you just need it for tests then it should be:
"net.sf.ehcache" % "ehcache" % ehcacheVersion % Test
test dependencies are generally fine, but I'd prefer not to add runtime dependencies to play-ahc-ws.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh got it, that makes sense, thanks for pointing that out.
|
||
cache { | ||
# Data that should be used to configure the cache | ||
caffeine { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need tests for custom configurations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Written a new test for this, the only way I could see for testing this is to make sure that the proper cache builder is set up depending on the cache name (because the name is passed to it from the CaffeineCacheManager.getCache).
|
||
## Setting the execution context | ||
|
||
By default, all Ehcache operations are blocking, and async implementations will block threads in the default execution context. | ||
By default, all Caffeine operations are blocking, and async implementations will block threads in the default execution context. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caffeine and Ehcache
_.overrides( | ||
bind[CaffeineCacheManager].toProvider[CustomCacheManagerProvider] | ||
).configure( | ||
"play.cache.createBoundCaches" -> false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I can tell this doesn't do anything for the caffeine implementation. This is meant to create a cache that's already in ehcache.xml
, so the whole test is probably useless. Instead we should include some tests exercising some caffeine cache specific configuration.
Usually this is okay if you are using Play's default configuration, which only stores elements in memory since reads should be relatively fast. | ||
However, depending on how EhCache was configured and [where the data is stored](http://www.ehcache.org/generated/2.10.4/html/ehc-all/#page/Ehcache_Documentation_Set%2Fco-store_storage_tiers.html), this blocking I/O might be too costly. | ||
For such a case you can configure a different [Akka dispatcher](http://doc.akka.io/docs/akka/current/scala/dispatchers.html#looking-up-a-dispatcher) and set it via `play.cache.dispatcher` so the EhCache plugin makes use of it: | ||
However, depending on how Caffeine Cache was configured, this blocking I/O might be too costly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this feature is really a lot more useful for Ehcache since it has slower storage tiers like caching to disk, so maybe worth keeping that part? /cc @mkurz.
|
||
If you want to access multiple different ehcache caches, then you'll need to tell Play to bind them in `application.conf`, like so: | ||
If you want to access multiple different caffeine cache caches, then you'll need to tell Play to bind them in `application.conf`, like so: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/caffeine cache caches/named caches/
|
||
@[jcache-sbt-dependencies](code/cache.sbt) | ||
|
||
If you are using Guice, you can add the following for Java annotations: | ||
|
||
@[jcache-guice-annotation-sbt-dependencies](code/cache.sbt) | ||
|
||
### JCache support with Caffeine | ||
|
||
Caffeine Cache does not natively implement the [JSR 107](https://github.com/jsr107/jsr107spec) specification, also known as JCache. If you want to use JCache with Caffeine you can use [caffeine-jcache](https://github.com/ben-manes/caffeine/wiki/JCache) dependency. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So actually I think the jcache
module we need is already there in playCaffeineDeps
so it shouldn't need to be added, right? And that documentation page just talks about configuring the JCache support for Caffeine, not how to add the dependency.
There is another confusing thing here: as far as I can tell, the JCache support for Caffeine would create a separate cache based on a separate configuration (which also uses typesafe config). @ben-manes what do you think is the best way to unify these two so we can have a common configuration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think unifying is a great option. All JCache implementations need to provide their own configuration support and each do it differently. Most appear to prefer XML, it seems. It would seem that you might want a generic integration strategy (e.g. Play JCache adapter) that fetches from the CacheManager
than to couple to an implementation.
Caffeine's JCache will likely be written in v3 to simplify it. The reason is that variable expiration was a late feature, since doing it correctly is hard, and most JCache implementations use lazy expiration by requiring a maximum size. This behavior was emulated in the JCache layer, and later a timerwheel-based approach was implemented in the core library. This will result in a simpler config and implementation, so I'd like the option to not tie it deeply with Play's.
Also, why does Play appear to recommend JCache? If you've looked into it, its actually an awful specification. Many of its features are incompatible with each other, it was rejected by JEE multiple times (most recently by JEE-8), and has a poor user experience. It is useful for integrations, like Hibernate, but otherwise I consider it to be a step backwards. I certainly wouldn't use it in any of my code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this documentation needs to be reworded since we should definitely not be recommending JCache.
I'm not actually sure why we introduced JCache support in the first place. Probably to allow integration with something else. @wsargent can you provide any background? I think it was used in the WS API to provide a common API for caching requests.
Maybe the rewording should be something like "We do not recommend using JCache in your own code, but it may be useful for integration with other libraries. Both Caffeine and Ehcache provide JCache implementations."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gmethvin I have added those sentences to the "JCache Support" header.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not actually sure why we introduced JCache support in the first place. Probably to allow integration with something else. @wsargent can you provide any background? I think it was used in the WS API to provide a common API for caching requests.
The theory was that we may add Caffeine, but people would still be using Ehcache (given how long its stuck around) and so JCache is the lowest common denominator API between them, same as reactive streams is the base API for akka streams. The purpose of JCache is as an integration layer, so by using that we wouldn't have to go through API rework depending on which library we picked later on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wsargent You are absolutely right. From what I've seen JCache API is used for ws caching only. Regular caching is implemented directly on top of plays CacheApi (lowest denominator) interface so it has no dependencies on JCache. I have removed the JCache section from the caching page but have not touched the ws documentation, ws still uses JCache but does a really good job of explaining how to use it so I figured better not touch those docs.
|
||
## Support for Caffeine | ||
|
||
Play now offeres an implementation of caching with Caffeine. Caffeine is the recomended cache implementation for Play users. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Play now offers a CacheApi implementation based on Caffeine." (with the link)
|
||
Play now offeres an implementation of caching with Caffeine. Caffeine is the recomended cache implementation for Play users. | ||
|
||
To migrate from EhCache to Caffeine you will have to remove `ehcache` from your dependencies and replace it with `caffeine` and you will have to change your caching configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Run-on sentence. Could be rewritten as: "To migrate from EhCache to Caffeine you will have to remove ehcache
from your dependencies and replace it with caffeine
. To customize the settings from the defaults you will also need to update the configuration in application.conf
as explained in the documentation."
Thanks @dusanstanojeviccs! @gmethvin are there remaining items for review? |
val cacheApi = app.injector.instanceOf[JavaAsyncCacheApi] | ||
Await.result(cacheApi.set("foo", "bar", 1 /* second */ ).toScala, 1.second) | ||
|
||
Thread.sleep(2.seconds.toMillis) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need these Thread.sleep
calls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that particular test sets expiration to 1 second, waits 2 seconds and check if the value really got cleared from the cache, that is why Thread.sleep is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have some of these in our specs. It could be good to move them later to use something like specs2 eventually
.
But this may be out of the scope of this PR.
|
||
For any data stored in the cache, a regeneration strategy needs to be put in place in case the data goes missing. This philosophy is one of the fundamentals behind Play, and is different from Java EE, where the session is expected to retain values throughout its lifetime. | ||
|
||
The default implementation of the cache API uses [Ehcache](http://www.ehcache.org/). | ||
Play provides a CacheApi implementation based on [Caffeine](https://github.com/ben-manes/caffeine/) and a legacy implementation based on [Ehcache 2.x](http://www.ehcache.org). For in-process caching Caffeine is typically the best choice. If you need distributed caching, there are third-party plugins for memcached and redis. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to link to https://github.com/mumoshu/play2-memcached and https://github.com/KarelCemus/play-redis.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe create a "Cache" section on ModuleDirectory.md
and link to there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made a bunch of smaller comments/questions/suggestions.
Thanks for the amazing work and patience here, @dusanstanojeviccs.
Caching data is a typical optimization in modern applications, and so Play provides a global cache. An important point about the cache is that it behaves just like a cache should: the data you just stored may just go missing. | ||
Caching data is a typical optimization in modern applications, and so Play provides a global cache. | ||
|
||
> An important point about the cache is that it behaves just like a cache should: the data you just stored may just go missing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick of docs formatting:
> **Note:** An important point ...
|
||
@[jcache-guice-annotation-sbt-dependencies](code/cache.sbt) | ||
The API dependency is useful if you'd like to define your own bindings for the `Cached` helper and `AsyncCacheApi`, etc., without having to depend on any specific cache implementation. If you're writing a custom cache module you should use this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add links to Cached
and AsyncCacheApi
:
... your own bindings for the [`Cached`](api/java/play/cache/Cached.html) helper and [`AsyncCacheApi`](api/java/play/cache/AsyncCacheApi.html), etc., without ...
Also, for Java Cached
is an annotation that does not have/need bindings.
) | ||
//#ehcache-sbt-dependencies | ||
|
||
//#jcache-sbt-dependencies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the reference for this snippet section was removed, just like jcache-guice-annotation-sbt-dependencies
a few lines below. The same for documentation/manual/working/javaGuide/main/cache/code/cache.sbt
.
|
||
@[jcache-guice-annotation-sbt-dependencies](code/cache.sbt) | ||
The API dependency is useful if you'd like to define your own bindings for the `Cached` helper and `AsyncCacheApi`, etc., without having to depend on any specific cache implementation. If you're writing a custom cache module you should use this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add links to Cached
and AsyncCacheApi
:
... bindings for the [`Cached`](api/scala/play/api/cache/Cached.html) helper and [`AsyncCacheApi`](api/scala/play/api/cache/AsyncCacheApi.html), etc., without ...
) | ||
//#ehcache-sbt-dependencies | ||
|
||
//#jcache-sbt-dependencies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same for Java examples, is this still being used?
framework/project/Dependencies.scala
Outdated
val ehcacheVersion = "2.10.4" | ||
val playEhcacheDeps = Seq( | ||
"net.sf.ehcache" % "ehcache" % ehcacheVersion, | ||
"org.ehcache" % "jcache" % "1.0.1" | ||
) ++ jcacheApi | ||
|
||
val caffeineVersion = "2.5.6" | ||
val caffeineVersion = "2.6.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use caffeine 2.6.2.
|
||
import com.github.benmanes.caffeine.cache.Expiry; | ||
|
||
import javax.annotation.Nonnull; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. We may want to start using these annotations more widely. :-)
val cacheApi = app.injector.instanceOf[JavaAsyncCacheApi] | ||
Await.result(cacheApi.set("foo", "bar", 1 /* second */ ).toScala, 1.second) | ||
|
||
Thread.sleep(2.seconds.toMillis) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have some of these in our specs. It could be good to move them later to use something like specs2 eventually
.
But this may be out of the scope of this PR.
"Java AsyncCacheApi" should { | ||
"set cache values" in new WithApplication { | ||
val cacheApi = app.injector.instanceOf[JavaAsyncCacheApi] | ||
Await.result(cacheApi.set("foo", "bar").toScala, 1.second) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just use await(cacheApi("foo", "bar").toScala)
instead, which already has a default timeout and is slightly less verbose. (await
is part of PlaySpecification
, see play.api.test.FutureAwaits
).
framework/build.sbt
Outdated
@@ -324,14 +325,24 @@ lazy val PlayEhcacheProject = PlayCrossBuiltProject("Play-Ehcache", "play-ehcach | |||
PlaySpecs2Project % "test" | |||
) | |||
|
|||
lazy val PlayCaffeineCacheProject = PlayCrossBuiltProject("Play-Caffeine-Cache", "play-caffeine-cache") | |||
.settings( | |||
libraryDependencies ++= playCaffeineDeps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also add the following setting here:
mimaPreviousArtifacts := Set.empty,
|
||
## Support for Caffeine | ||
|
||
Play now offers a CacheApi implementation based on [Caffeine](https://github.com/ben-manes/caffeine/). Caffeine is the recomended cache implementation for Play users. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: recommended
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
Thank you so much, @dusanstanojeviccs. This is a fantastic contribution.
defaults { | ||
initial-capacity = null | ||
weak-keys = null | ||
weak-keys = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dusanstanojeviccs I think you made a mistake here? weak-keys
twice? Shouldn't be one of both configs be weak-values
? See
playframework/cache/play-caffeine-cache/src/main/java/play/cache/caffeine/CaffeineParser.java
Lines 68 to 70 in bc1badd
case "weak-values": | |
conditionally(key, cacheBuilder::weakValues); | |
break; |
Also the config
maximum-size = null
is missing: playframework/cache/play-caffeine-cache/src/main/java/play/cache/caffeine/CaffeineParser.java
Lines 60 to 64 in bc1badd
case "maximum-size": | |
if (!config.getIsNull(key)) { | |
cacheBuilder.maximumSize(config.getLong(key)); | |
} | |
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix can be found here: #10398
Pull Request Checklist
Helpful things
Fixes
Fixes #7974
Purpose
This PR is adding Caffeine Cache module to Play and setting Caffeine Cache as the default cache provider.
Background Context
I've mostly based this solution on previous Ehcache implementation.
References
#7974