io.helidon.security.providers.EvictableCacheImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of helidon-security-providers-common Show documentation
Show all versions of helidon-security-providers-common Show documentation
Common utilities for providers.
/*
* Copyright (c) 2018 Oracle and/or its affiliates. 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 io.helidon.security.providers;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Supplier;
/**
* Default implementation of {@link EvictableCache}.
*/
class EvictableCacheImpl implements EvictableCache {
/**
* Number of threads in the scheduled thread pool to evict records.
*/
private static final int EVICT_THREAD_COUNT = 1;
private static final ScheduledThreadPoolExecutor EXECUTOR;
/**
* An implementation that does no caching.
*/
static final EvictableCache, ?> NO_CACHE = new EvictableCache() { };
static {
ThreadFactory jf = new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, getClass().getSimpleName() + "-cachePurge_" + counter.getAndIncrement());
}
};
EXECUTOR = new ScheduledThreadPoolExecutor(EVICT_THREAD_COUNT, jf);
}
private final ConcurrentHashMap> cacheMap = new ConcurrentHashMap<>();
private final long cacheTimoutNanos;
private final long cacheMaxSize;
private final long evictParallelismThreshold;
private final ScheduledFuture> evictionFuture;
private final BiFunction evictor;
EvictableCacheImpl(Builder builder) {
cacheMaxSize = builder.cacheMaxSize();
cacheTimoutNanos = TimeUnit.NANOSECONDS.convert(builder.cacheTimeout(), builder.cacheTimeoutUnit());
evictParallelismThreshold = builder.parallelismThreshold();
evictor = builder.evictor();
evictionFuture = EXECUTOR.scheduleAtFixedRate(
this::evict,
builder.cacheEvictDelay(),
builder.cacheEvictPeriod(),
builder.cacheEvictTimeUnit());
EXECUTOR.setRemoveOnCancelPolicy(true);
}
@Override
public Optional remove(K key) {
CacheRecord removed = cacheMap.remove(key);
if (null == removed) {
return Optional.empty();
}
return validate(removed).map(CacheRecord::getValue);
}
@Override
public Optional get(K key) {
return getRecord(key).flatMap(this::validate).map(CacheRecord::getValue);
}
@Override
public int size() {
return cacheMap.size();
}
@Override
public Optional computeValue(K key, Supplier> valueSupplier) {
try {
return doComputeValue(key, valueSupplier);
} catch (CacheFullException e) {
return valueSupplier.get();
}
}
@Override
public void close() {
evictionFuture.cancel(true);
cacheMap.clear();
}
void evict() {
cacheMap.forEachKey(evictParallelismThreshold, key -> cacheMap.compute(key, (key1, cacheRecord) -> {
if ((null == cacheRecord) || evictor.apply(cacheRecord.getKey(), cacheRecord.getValue())) {
return null;
} else {
if (cacheRecord.isValid(cacheTimoutNanos)) {
return cacheRecord;
} else {
return null;
}
}
}));
}
private Optional> validate(CacheRecord record) {
if (record.isValid(cacheTimoutNanos) && !evictor.apply(record.getKey(), record.getValue())) {
return Optional.of(record);
}
cacheMap.remove(record.key);
return Optional.empty();
}
private Optional doComputeValue(K key, Supplier> valueSupplier) {
CacheRecord record = cacheMap.compute(key, (s, cacheRecord) -> {
if ((null != cacheRecord) && cacheRecord.isValid(cacheTimoutNanos)) {
cacheRecord.accessed();
return cacheRecord;
}
if (cacheMap.size() >= cacheMaxSize) {
throw new CacheFullException();
}
return valueSupplier.get()
.map(v -> new CacheRecord<>(key, v))
.orElse(null);
});
if (null == record) {
return Optional.empty();
} else {
return Optional.of(record.value);
}
}
private Optional> getRecord(K key) {
return Optional.ofNullable(cacheMap.get(key));
}
private static final class CacheRecord {
private final K key;
private final V value;
private volatile long lastAccess = System.nanoTime();
private CacheRecord(K key, V value) {
this.key = key;
this.value = value;
}
private void accessed() {
lastAccess = System.nanoTime();
}
private boolean isValid(long timeoutNanos) {
return (System.nanoTime() - lastAccess) < timeoutNanos;
}
private K getKey() {
return key;
}
private V getValue() {
return value;
}
}
private static final class CacheFullException extends RuntimeException {
private CacheFullException() {
}
}
}