Skip to content

Commit

Permalink
Integrate variable expiration with JCache
Browse files Browse the repository at this point in the history
JCache has its own expiration policy, but unfortunately differences in
the API and the requirement to retain existing functionality means that
the two cannot be merged yet.

The settings currently allow using both variable and fixed expiration
together. This is because the spec authors assumed that a maximum size
would also be enabled, thereby evicting expired entries lazily. Since
we exposed Caffeine's eager policy, then only for fixed, these two
could be used together. With Caffeine's new variable policy, it does
not allow the combination of fixed and variable in the core library.
Thus, lazy-expiration cannot be mapped directly to Caffeine's version.

To retain the previous combinations, we could adapt only when the
fixed settings are not used. Then supply an eternal no-op policy for
the lazy logic and an adapter for the eager logic. Unfortunately API
differences makes this more difficult. For example, JCache's
putIfAbsent does not return a value when failing and therefore is not
a read. Caffeine's does, which results in TCK failures when the expiry
policy calls are verified.

A possible solution would be to use a ThreadLocal to signal the cases
when the expiry calls should be ignored. That adds brittleness to an
already complex adapter. Instead, perhaps in v3 we can remove the lazy
and fixed settings, simplify the adapter, and use the ThreadLocal
trick to correct these edge cases.

Given that the major JCache implementors are lazy, expect a maximum,
and that JCache's ExpiryPolicy is neutered by not providing the key or
value, I don't think a hacky solution is worth the effort. Users can
use Caffeine's expiry if they need eager behavior.
  • Loading branch information
ben-manes committed May 8, 2017
1 parent fc57249 commit 1cc9eb0
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2239,9 +2239,11 @@ V remap(K key, Object keyRef, BiFunction<? super K, ? super V, ? extends V> 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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ private final class Builder<K, V> {
/** Creates a configured cache. */
public CacheProxy<K, V> build() {
boolean evicts = configureMaximumSize() || configureMaximumWeight()
|| configureExpireAfterWrite() || configureExpireAfterAccess();
|| configureExpireAfterWrite() || configureExpireAfterAccess()
|| configureExpireVariably();
if (evicts) {
configureEvictionListener();
}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -52,6 +54,7 @@ public final class CaffeineConfiguration<K, V> implements CompleteConfiguration<
private final MutableConfiguration<K, V> delegate;

private Factory<Weigher<K, V>> weigherFactory;
private Factory<Expiry<K, V>> expiryFactory;
private Factory<Copier> copierFactory;
private Factory<Ticker> tickerFactory;

Expand All @@ -75,6 +78,7 @@ public CaffeineConfiguration(CompleteConfiguration<K, V> configuration) {
refreshAfterWriteNanos = config.refreshAfterWriteNanos;
expireAfterAccessNanos = config.expireAfterAccessNanos;
expireAfterWriteNanos = config.expireAfterWriteNanos;
expiryFactory = config.expiryFactory;
copierFactory = config.copierFactory;
tickerFactory = config.tickerFactory;
weigherFactory = config.weigherFactory;
Expand Down Expand Up @@ -250,6 +254,17 @@ public void setTickerFactory(Factory<Ticker> 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.
*
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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<Factory<Expiry<K, V>>> 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<? extends Expiry<K, V>>> factory) {
expiryFactory = (Factory<Expiry<K, V>>) factory.orElse(null);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
13 changes: 9 additions & 4 deletions jcache/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <tt>variable expiry</tt> policy is configured.
*
* @author [email protected] (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<Integer, Integer> 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<Integer, Integer> getConfiguration() {
CaffeineConfiguration<Integer, Integer> 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());
}
}

0 comments on commit 1cc9eb0

Please sign in to comment.