diff --git a/.travis.yml b/.travis.yml index 9d6bc21ef0..110d3bd421 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ script: - sh -c 'cd examples/write-behind-rxjava && mvn test' after_success: -- ./gradlew coveralls uploadArchives -x test +- ./gradlew coveralls uploadArchives -x test -x isolatedTests matrix: fast_finish: true diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java index a7005016a0..cd893bd3c6 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java @@ -2239,9 +2239,11 @@ V remap(K key, Object keyRef, BiFunction rema weight[0] = n.getWeight(); weight[1] = weigher.weigh(key, newValue[0]); - if ((cause[0] == null) && (newValue[0] != oldValue[0])) { + if (cause[0] == null) { + if (newValue[0] != oldValue[0]) { + cause[0] = RemovalCause.REPLACED; + } setVariableTime(n, expireAfterUpdate(n, key, newValue[0], now)); - cause[0] = RemovalCause.REPLACED; } else { setVariableTime(n, expireAfterCreate(key, newValue[0], now)); } diff --git a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheFactory.java b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheFactory.java index 5377715ebd..1f332ffe58 100644 --- a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheFactory.java +++ b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheFactory.java @@ -136,7 +136,8 @@ private final class Builder { /** Creates a configured cache. */ public CacheProxy build() { boolean evicts = configureMaximumSize() || configureMaximumWeight() - || configureExpireAfterWrite() || configureExpireAfterAccess(); + || configureExpireAfterWrite() || configureExpireAfterAccess() + || configureExpireVariably(); if (evicts) { configureEvictionListener(); } @@ -209,6 +210,12 @@ private boolean configureExpireAfterAccess() { return config.getExpireAfterAccess().isPresent(); } + /** Configures the write expiration and returns if set. */ + private boolean configureExpireVariably() { + config.getExpiryFactory().ifPresent(factory -> caffeine.expireAfter(factory.create())); + return config.getExpireAfterWrite().isPresent(); + } + private boolean configureRefreshAfterWrite() { if (config.getRefreshAfterWrite().isPresent()) { caffeine.refreshAfterWrite(config.getRefreshAfterWrite().getAsLong(), TimeUnit.NANOSECONDS); diff --git a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/CaffeineConfiguration.java b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/CaffeineConfiguration.java index fdbf076c7b..33df039c55 100644 --- a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/CaffeineConfiguration.java +++ b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/CaffeineConfiguration.java @@ -18,6 +18,7 @@ import static java.util.Objects.requireNonNull; import java.util.Objects; +import java.util.Optional; import java.util.OptionalLong; import javax.annotation.Nullable; @@ -29,6 +30,7 @@ import javax.cache.integration.CacheLoader; import javax.cache.integration.CacheWriter; +import com.github.benmanes.caffeine.cache.Expiry; import com.github.benmanes.caffeine.cache.Ticker; import com.github.benmanes.caffeine.cache.Weigher; import com.github.benmanes.caffeine.jcache.copy.Copier; @@ -52,6 +54,7 @@ public final class CaffeineConfiguration implements CompleteConfiguration< private final MutableConfiguration delegate; private Factory> weigherFactory; + private Factory> expiryFactory; private Factory copierFactory; private Factory tickerFactory; @@ -75,6 +78,7 @@ public CaffeineConfiguration(CompleteConfiguration configuration) { refreshAfterWriteNanos = config.refreshAfterWriteNanos; expireAfterAccessNanos = config.expireAfterAccessNanos; expireAfterWriteNanos = config.expireAfterWriteNanos; + expiryFactory = config.expiryFactory; copierFactory = config.copierFactory; tickerFactory = config.tickerFactory; weigherFactory = config.weigherFactory; @@ -250,6 +254,17 @@ public void setTickerFactory(Factory factory) { tickerFactory = requireNonNull(factory); } + /** + * Returns the refresh after write in nanoseconds. + * + * @return the duration in nanoseconds + */ + public OptionalLong getRefreshAfterWrite() { + return (refreshAfterWriteNanos == null) + ? OptionalLong.empty() + : OptionalLong.of(refreshAfterWriteNanos); + } + /** * Set the refresh after write in nanoseconds. * @@ -262,14 +277,14 @@ public void setRefreshAfterWrite(OptionalLong refreshAfterWriteNanos) { } /** - * Returns the refresh after write in nanoseconds. + * Returns the expire after write in nanoseconds. * * @return the duration in nanoseconds */ - public OptionalLong getRefreshAfterWrite() { - return (refreshAfterWriteNanos == null) + public OptionalLong getExpireAfterWrite() { + return (expireAfterWriteNanos == null) ? OptionalLong.empty() - : OptionalLong.of(refreshAfterWriteNanos); + : OptionalLong.of(expireAfterWriteNanos); } /** @@ -284,14 +299,14 @@ public void setExpireAfterWrite(OptionalLong expireAfterWriteNanos) { } /** - * Returns the expire after write in nanoseconds. + * Returns the expire after access in nanoseconds. * * @return the duration in nanoseconds */ - public OptionalLong getExpireAfterWrite() { - return (expireAfterWriteNanos == null) + public OptionalLong getExpireAfterAccess() { + return (expireAfterAccessNanos == null) ? OptionalLong.empty() - : OptionalLong.of(expireAfterWriteNanos); + : OptionalLong.of(expireAfterAccessNanos); } /** @@ -306,14 +321,22 @@ public void setExpireAfterAccess(OptionalLong expireAfterAccessNanos) { } /** - * Returns the expire after access in nanoseconds. + * Returns the {@link Factory} for the {@link Expiry} to be used for the cache. * - * @return the duration in nanoseconds + * @return the {@link Factory} for the {@link Expiry} */ - public OptionalLong getExpireAfterAccess() { - return (expireAfterAccessNanos == null) - ? OptionalLong.empty() - : OptionalLong.of(expireAfterAccessNanos); + public Optional>> getExpiryFactory() { + return Optional.ofNullable(expiryFactory); + } + + /** + * Set the {@link Factory} for the {@link Expiry}. + * + * @param factory the {@link Expiry} {@link Factory} + */ + @SuppressWarnings("unchecked") + public void setExpiryFactory(Optional>> factory) { + expiryFactory = (Factory>) factory.orElse(null); } /** diff --git a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurator.java b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurator.java index f7905cc0be..d5fb3c1d1d 100644 --- a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurator.java +++ b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/configuration/TypesafeConfigurator.java @@ -219,6 +219,10 @@ public void addEagerExpiration() { long nanos = expiration.getDuration("after-access", TimeUnit.NANOSECONDS); configuration.setExpireAfterAccess(OptionalLong.of(nanos)); } + if (expiration.hasPath("variable")) { + configuration.setExpiryFactory(Optional.of( + FactoryBuilder.factoryOf(expiration.getString("variable")))); + } } /** Adds the Caffeine refresh settings. */ diff --git a/jcache/src/main/resources/reference.conf b/jcache/src/main/resources/reference.conf index 292c6a2aa1..100988b586 100644 --- a/jcache/src/main/resources/reference.conf +++ b/jcache/src/main/resources/reference.conf @@ -48,7 +48,7 @@ caffeine.jcache { # The eviction policy for automatically removing entries from the cache policy { # The expiration threshold before lazily evicting an entry. This single threshold is reset on - # every operation where a duration is specified. As required by the specification, if an entry + # every operation where a duration is specified. As expected by the specification, if an entry # expires but is not accessed and no resource constraints force eviction, then the expired # entry remains in place. lazy-expiration { @@ -68,19 +68,24 @@ caffeine.jcache { access = "eternal" } - # The expiration thresholds before eagerly evicting an entry. This settings correspond to the + # The expiration thresholds before eagerly evicting an entry. These settings correspond to the # expiration supported natively by Caffeine where expired entries are collected during # maintenance operations. eager-expiration { # Specifies that each entry should be automatically removed from the cache once a fixed # duration has elapsed after the entry's creation, or the most recent replacement of its - # value. + # value. This setting cannot be combined with the variable configuration. after-write = null # Specifies that each entry should be automatically removed from the cache once a fixed # duration has elapsed after the entry's creation, the most recent replacement of its value, - # or its last read. Access time is reset by all cache read and write operation. + # or its last read. Access time is reset by all cache read and write operation. This setting + # cannot be combined with the variable configuration. after-access = null + + # The expiry class to use when calculating the expiration time of cache entries. This + # setting cannot be combined with after-write or after-access configurations. + variable = null } # The threshold before an entry is eligible to be automatically refreshed when the first stale diff --git a/jcache/src/test/java/com/github/benmanes/caffeine/jcache/expiry/JCacheExpiryTest.java b/jcache/src/test/java/com/github/benmanes/caffeine/jcache/expiry/JCacheExpiryTest.java new file mode 100644 index 0000000000..b419675807 --- /dev/null +++ b/jcache/src/test/java/com/github/benmanes/caffeine/jcache/expiry/JCacheExpiryTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016 Ben Manes. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.benmanes.caffeine.jcache.expiry; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.mockito.Mockito; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.github.benmanes.caffeine.cache.Expiry; +import com.github.benmanes.caffeine.jcache.AbstractJCacheTest; +import com.github.benmanes.caffeine.jcache.configuration.CaffeineConfiguration; + +/** + * The test cases that ensure the variable expiry policy is configured. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +@Test(singleThreaded = true) +@SuppressWarnings("unchecked") +public final class JCacheExpiryTest extends AbstractJCacheTest { + private static final long ONE_MINUTE = TimeUnit.MINUTES.toNanos(1); + + private Expiry expiry = Mockito.mock(Expiry.class); + + @BeforeMethod + public void setup() { + Mockito.reset(expiry); + when(expiry.expireAfterCreate(any(), any(), anyLong())).thenReturn(ONE_MINUTE); + when(expiry.expireAfterUpdate(any(), any(), anyLong(), anyLong())).thenReturn(ONE_MINUTE); + when(expiry.expireAfterRead(any(), any(), anyLong(), anyLong())).thenReturn(ONE_MINUTE); + } + + @Override + protected CaffeineConfiguration getConfiguration() { + CaffeineConfiguration configuration = new CaffeineConfiguration<>(); + configuration.setExpiryFactory(Optional.of(() -> expiry)); + configuration.setTickerFactory(() -> ticker::read); + return configuration; + } + + @Test + public void configured() { + jcache.put(KEY_1, VALUE_1); + verify(expiry, times(1)).expireAfterCreate(any(), any(), anyLong()); + + jcache.put(KEY_1, VALUE_2); + verify(expiry).expireAfterUpdate(any(), any(), anyLong(), anyLong()); + + jcache.get(KEY_1); + verify(expiry).expireAfterRead(any(), any(), anyLong(), anyLong()); + } +}