package org.infernus.idea.checkstyle.checker; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Supplier; import java.util.stream.Collectors; import com.intellij.concurrency.JobScheduler; import com.intellij.openapi.module.Module; import org.infernus.idea.checkstyle.model.ConfigurationLocation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class CheckerFactoryCache { private static final int CLEANUP_PERIOD_SECONDS = 30; // TODO This may work more reliably if we just used a ConcurrentHashMap instead of our own reimplementation. // TODO We should check for expiration only when we return a Checker from the cache, so that we don't need the // backgroundCleanupTask. private final ReadWriteLock cacheLock = new ReentrantReadWriteLock(); private final Map<CheckerFactoryCacheKey, CachedChecker> cache = new HashMap<>(); private final ScheduledExecutorService cleanUpExecutor = JobScheduler.getScheduler(); public CheckerFactoryCache() { startBackgroundCleanupTask(); } // TODO Why would a cache return Optionals? The value should either be present or not present. public Optional<CachedChecker> get(@NotNull final ConfigurationLocation location, @Nullable final Module module) { final CheckerFactoryCacheKey key = new CheckerFactoryCacheKey(location, module); cacheLock.readLock().lock(); try { if (cache.containsKey(key)) { final CachedChecker cachedChecker = cache.get(key); if (cachedChecker != null && cachedChecker.isValid()) { return Optional.of(cachedChecker); } else { cacheLock.readLock().unlock(); writeToCache(() -> { try { if (cachedChecker != null) { cachedChecker.destroy(); } } finally { return cache.remove(key); } }); cacheLock.readLock().lock(); } } return Optional.empty(); } finally { cacheLock.readLock().unlock(); } } public void put(@NotNull final ConfigurationLocation location, final Module module, @NotNull final CachedChecker checker) { writeToCache(() -> cache.put(new CheckerFactoryCacheKey(location, module), checker)); } public void invalidate() { writeToCache(() -> { try { cache.values().forEach(this::destroyChecker); } finally { cache.clear(); } return null; }); } private void destroyChecker(@NotNull final CachedChecker pCachedChecker) { pCachedChecker.destroy(); } private void startBackgroundCleanupTask() { // Use {@link ScheduleThreadPoolExecutor#scheduleWithFixedDelay(Runnable, long, long, TimeUnit)} rather // than {@link ScheduleThreadPoolExecutor#scheduleWithFixedRate(Runnable, long, long, TimeUnit)} as // recommended by JetBrains for compatibility with hibernation. cleanUpExecutor.scheduleWithFixedDelay(this::cleanUpExpiredCachedCheckers, CLEANUP_PERIOD_SECONDS, CLEANUP_PERIOD_SECONDS, TimeUnit.SECONDS); } private void cleanUpExpiredCachedCheckers() { writeToCache(() -> { final List<CheckerFactoryCacheKey> itemsToRemove = cache.entrySet().stream().filter(cacheEntry -> cacheEntry.getValue() != null && !cacheEntry.getValue().isValid()).map(cacheEntry -> { destroyChecker(cacheEntry.getValue()); return cacheEntry.getKey(); }).collect(Collectors.toList()); return cache.entrySet().removeIf(entry -> itemsToRemove.contains(entry.getKey())); }); } private <T> T writeToCache(final Supplier<T> task) { cacheLock.writeLock().lock(); try { return task.get(); } finally { cacheLock.writeLock().unlock(); } } }