-
-
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
Updates cache api to make getOrElseUpdate methods atomic #9615
Updates cache api to make getOrElseUpdate methods atomic #9615
Conversation
case Some(value) => Future.successful(value) | ||
case None => orElse.flatMap(value => set(key, value, expiration).map(_ => value)) | ||
} | ||
orElse.map(orElseValue => sync.getOrElseUpdate(key, expiration)(orElseValue)) |
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 always compute a new future and maybe add it to the cache?
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.
actually it should be computed and put if there is no value with the given 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.
It just reads to me, not having written scala in many years, that it always computes a new future value. I think the intent is to only compute if absent. Could this class use the AsyncCache in caffeine instead of the synchronous one?
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 asked the same question in pr description, that bothered me as well 😃
I don't know why AsyncCacheApi does not use the AsyncCache of Caffeine, that way AsyncCacheApi.getOrElseUpdate could lazily evaluate orElse part
I couldn't find a way to make it atomic and evaulate orElse lazily with the sync api.
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 know why AsyncCacheApi does not use the AsyncCache of Caffeine, that way AsyncCacheApi.getOrElseUpdate could lazily evaluate orElse part
I would say the main reason is that then access from both async and sync cache can keep cached data consistent. It is also more convenient to not implement them both from scratch so it is less code to maintain, test, etc.
But I suspect we can implement play.cache.caffeine.NamedCaffeineCache
in terms of com.github.benmanes.caffeine.cache.AsyncCache
instead and have some sync methods that will just wrap values using CompletableFuture.completedFuture
. Another possible constraint is if EhCache has a way to do that.
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.
Note that AsyncCache
has a synchronous()
method to provide a Cache
view. The async cache internally maintains K=>Future<V>
and the synchronous view tries to provide a K=>V
view by blocking on the future as appropriate. That may be useful in simplifying the 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.
That makes sense to me.
It would be better to open a new PR though, so it will be focused on this other change and easier to review.
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.
ok, I'll open a new pr for using com.github.benmanes.caffeine.cache.AsyncCache
and update on this pr on top of that, thanks @ben-manes, @marcospereira
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.
hi @marcospereira, I created a pr for this #9623
currentDuration | ||
} | ||
|
||
def expireAfterRead(key: String, value: ExpirableCacheValue[Any], currentTime: Long, currentDuration: Long): Long = { |
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 understand correctly, this will reset the duration for every read, which is not the existing behavior for policy().expireVariably().get().put(...)
. Is that correct, @ben-manes?
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 called on every read, however this implementation returns currentDuration
. This indicates that no change to the entry's lifetime should occur due to this read, and the cache will not modify the lifetime.
It looks like this configuration is that a lifetime is set by its creation time, and not modified thereafter. The set(k, v, duration)
may modify the entry's lifetime on an update. So I believe that the expireAfterUpdate
may need similar logic to expireAfterCreate
.
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're right policy().expireVariably().get().put
does not use CaffeineDefaultExpiry
, it uses a custom Expiry
that behaves same on expireAfterCreate
and expireAfterUpdate
methods
cache.policy().expireVariably().get().put(key, value, Long.MaxValue, TimeUnit.DAYS) | ||
case finite: FiniteDuration => | ||
val seconds = finite.toSeconds | ||
if (seconds <= 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.
I don't think we are handling this case where the duration is negative anymore. Ideally, if there is no test for it, we should add one to ensure we don't have a regression here.
case Some(value) => Future.successful(value) | ||
case None => orElse.flatMap(value => set(key, value, expiration).map(_ => value)) | ||
} | ||
orElse.map(orElseValue => sync.getOrElseUpdate(key, expiration)(orElseValue)) |
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 know why AsyncCacheApi does not use the AsyncCache of Caffeine, that way AsyncCacheApi.getOrElseUpdate could lazily evaluate orElse part
I would say the main reason is that then access from both async and sync cache can keep cached data consistent. It is also more convenient to not implement them both from scratch so it is less code to maintain, test, etc.
But I suspect we can implement play.cache.caffeine.NamedCaffeineCache
in terms of com.github.benmanes.caffeine.cache.AsyncCache
instead and have some sync methods that will just wrap values using CompletableFuture.completedFuture
. Another possible constraint is if EhCache has a way to do that.
…e interface as discussed playframework#9615
…e interface as discussed playframework#9615
…e interface as discussed playframework#9615
a70b2ba
to
d3efbdc
Compare
updated cache apis to make getOrElseUpdate methods atomic
…efaultExpiry.java for this reason "class play.cache.caffeine.CaffeineDefaultExpiry does not have a correspondent in current version"
…xpiry" from binary compatibility
d3efbdc
to
4b7e42e
Compare
@gurkankaymak I saw you pushed some commits here. Is there still something missing or is this pull request ready for another review? |
…he getOrElseUpdate method
@mkurz just pushed a new commit in the EhCache part, pr could be reviewed 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.
Thanks, @gurkankaymak.
I think it will also be useful to add some details about this change in our migration guide. For example, when using EhCache, the orElse
part is now eargerly computed even when the value is present. This may be surprising to users, and since we plan to move EhCache support out of Play, I would say that it would be better to not bother changing it (the migration section can then focus exclusively on Caffeine implementation which is the default one).
WDYT?
cache/play-caffeine-cache/src/main/java/play/cache/caffeine/CaffeineDefaultExpiry.java
Show resolved
Hide resolved
cache/play-caffeine-cache/src/main/scala/play/api/cache/caffeine/CaffeineCacheApi.scala
Outdated
Show resolved
Hide resolved
cache/play-caffeine-cache/src/main/scala/play/api/cache/caffeine/DefaultCaffeineExpiry.scala
Outdated
Show resolved
Hide resolved
cache/play-caffeine-cache/src/main/scala/play/api/cache/caffeine/ExpirableCacheValue.scala
Outdated
Show resolved
Hide resolved
cache/play-caffeine-cache/src/test/scala/play/api/cache/caffeine/CaffeineCacheApiSpec.scala
Outdated
Show resolved
Hide resolved
- made ExpirableCacheValue.scala and DefaultCaffeineExpiry.scala package private
other than eagerly evaluating the
|
updated Cache APIs to make getOrElseUpdate methods atomic
cache/play-caffeine-cache/src/main/scala/play/api/cache/caffeine/CaffeineCacheApi.scala
Outdated
Show resolved
Hide resolved
I would suggest leaving the ehcache version non-atomic as always evaluating defeats the purpose of a cache. Instead it seems like an incentive to migrate. There are probably other implementations, like redis/memcached, which would not be atomic either. |
It looks like to me that it would be better not to touch the EhCache implementation, but instead only the Caffeine one and make it clear at the migration guide that EhCache's
And document these on the migration guide. Does that make sense to you? Best. |
I see your point, |
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.
Some minor comments, but I think we are almost there. Thanks for your patience, @gurkankaymak. Things to do before merging this:
- Squashing commits to organize them around the final changes better
- Add tests to ensure
orElse
is NOT computed when the cache (sync and async) has the value
Besides that, it is looking good to me.
cache/play-caffeine-cache/src/main/java/play/cache/caffeine/CaffeineDefaultExpiry.java
Show resolved
Hide resolved
cache/play-caffeine-cache/src/main/scala/play/api/cache/caffeine/CaffeineCacheApi.scala
Outdated
Show resolved
Hide resolved
updated CaffeineCacheApi to make getOrElseUpdate methods atomic
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, for what that's worth!
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, @gurkankaymak!
|
||
private def calculateExpirationTime(durationMaybe: Option[Duration]): Long = { | ||
durationMaybe match { | ||
case Some(duration) if duration.isFinite && duration.lteq(0.second) => 1.second.toNanos |
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.
@gurkankaymak it's been a while, but do you remember why you gave the element 1 second to live? My guess was you did that because back then the ehcache implementation set the time to life for an element to 1 second as well if expiration was actually set to 0 seconds.
See this comment: https://github.com/playframework/playframework/pull/5923/files#r1005645691
Or was there another reason for doing that? Because I will set that to 0 in #11516 as I can see no sense in 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.
@mkurz I don't think there was a specific reason for it, I agree with your guess, probably I was preserving the behaviour at the time while making the methods atomic
fixes #8605
Hi, updated cache api to make getOrElseUpdate methods atomic
I don't know why
AsyncCacheApi
does not use theAsyncCache
ofCaffeine
, that wayAsyncCacheApi.getOrElseUpdate
could lazily evaluateorElse
part.I'm not sure if the current implementation of
AsyncCacheApi.getOrElseUpdate
is acceptable, I couldn't find a way to make it atomic and evaulate orElse lazily with thesync
api. Maybe just for this method the previous way could more preferable (get-if not found-put instead of atomic)I could update the pr according to your comments/recommendations