org.rapidoid.cache.impl.ConcurrentCacheAtom Maven / Gradle / Ivy
/*-
* #%L
* rapidoid-commons
* %%
* Copyright (C) 2014 - 2018 Nikolche Mihajlovski and contributors
* %%
* 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.
* #L%
*/
package org.rapidoid.cache.impl;
import org.rapidoid.RapidoidThing;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.cache.CacheAtom;
import org.rapidoid.lambda.Mapper;
import org.rapidoid.u.U;
import org.rapidoid.util.Resetable;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Authors("Nikolche Mihajlovski")
@Since("5.3.0")
public class ConcurrentCacheAtom extends RapidoidThing implements CacheAtom, Callable {
protected final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
protected final K key;
protected final Mapper loader;
protected final long ttlInMs;
protected volatile CachedValue cachedValue;
long approxAccessCounter;
public ConcurrentCacheAtom(K key, Mapper loader, long ttlInMs) {
this.key = key;
this.loader = loader;
this.ttlInMs = ttlInMs;
}
/**
* {@inheritDoc}
*/
@Override
public V get() {
return retrieveCachedValue(true, true);
}
/**
* {@inheritDoc}
*/
@Override
public V getIfExists() {
return retrieveCachedValue(false, true);
}
/**
* Retrieves the cached value, and maybe recalculates/reloads it if expired.
*
* The synchronization is based on the {@link ReentrantReadWriteLock} documentation.
*/
private V retrieveCachedValue(boolean loadIfExpired, boolean updateStats) {
V result;
V oldValue = null;
Throwable error = null;
boolean missed = false;
// race conditions are allowed
this.approxAccessCounter++;
long now;
CachedValue cached = cachedValue; // read the cached value
if (cached != null) {
long expiresAt = cached.expiresAt;
if (expiresAt == Long.MAX_VALUE) {
updateStats(false, false);
return cached.value;
}
now = U.time();
if (now <= expiresAt) {
updateStats(false, false);
return cached.value;
}
} else {
now = U.time();
}
// ----------------- START LOCKS -----------------
readLock();
cached = cachedValue; // read the cached value again, inside the read lock
if (cached == null || now > cached.expiresAt) {
readUnlock();
writeLock();
// double-check: another thread might have acquired write lock and changed state already
if (cached == null || now > cached.expiresAt) {
V newValue;
if (loadIfExpired) {
try {
newValue = loader != null ? loader.map(key) : null;
} catch (Throwable e) {
error = e;
newValue = null;
}
} else {
newValue = null;
}
oldValue = setValueInsideWriteLock(newValue);
missed = true;
}
// downgrade by acquiring read lock before releasing write lock
readLock();
writeUnlock();
}
cached = cachedValue;
result = cached != null ? cached.value : null; // read the cached value
readUnlock();
releaseOldValue(oldValue); // release the old value - outside of lock
if (updateStats) {
updateStats(missed, error != null);
}
if (error != null) {
throw U.rte("Couldn't recalculate the cache value!", error);
}
return result;
}
private void readLock() {
try {
if (!lock.readLock().tryLock(10, TimeUnit.SECONDS)) {
throw new RuntimeException("Couldn't acquire READ lock!");
}
} catch (InterruptedException e) {
throw new CancellationException();
}
}
private void readUnlock() {
lock.readLock().unlock();
}
private void writeLock() {
try {
if (!lock.writeLock().tryLock(10, TimeUnit.SECONDS)) {
throw new RuntimeException("Couldn't acquire WRITE lock!");
}
} catch (InterruptedException e) {
throw new CancellationException();
}
}
private void writeUnlock() {
lock.writeLock().unlock();
}
protected void updateStats(boolean missed, boolean hasError) {
// do nothing
}
/**
* {@inheritDoc}
*/
@Override
public void set(V value) {
writeLock();
V oldValue = setValueInsideWriteLock(value);
writeUnlock();
releaseOldValue(oldValue); // release the old value - outside of lock
}
/**
* Sets new cached value, executes inside already acquired write lock.
*/
private V setValueInsideWriteLock(V newValue) {
CachedValue cached = cachedValue; // read the cached value
V oldValue = cached != null ? cached.value : null;
if (newValue != null) {
long expiresAt = ttlInMs > 0 ? U.time() + ttlInMs : Long.MAX_VALUE;
cachedValue = new CachedValue<>(newValue, expiresAt);
} else {
cachedValue = null;
}
return oldValue;
}
/**
* {@inheritDoc}
*/
@Override
public void invalidate() {
writeLock();
V oldValue = setValueInsideWriteLock(null);
writeUnlock();
releaseOldValue(oldValue); // release the old value - outside of lock
}
/**
* Clean-up of an old, previously cached value.
*
* @param oldValue can be null
*/
private void releaseOldValue(V oldValue) {
if (oldValue instanceof Resetable) {
((Resetable) oldValue).reset();
}
}
void checkTTL() {
retrieveCachedValue(false, false);
}
@Override
public V call() throws Exception {
return get();
}
@Override
public String toString() {
return "ConcurrentCacheAtom{" +
"lock=" + lock +
", loader=" + loader +
", ttlInMs=" + ttlInMs +
", cachedValue=" + cachedValue +
'}';
}
}