/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package com.github.geophile.erdo.immutableitemcache; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; // Cache of items that are never updated. Because the items never change, there are no dirty items that need to // be written back anywhere, which keeps things extremely simple. // ID: The key of the items in the cache. // ITEM: What's in the cache. public class ImmutableItemCache<ID, ITEM extends CacheEntry<ID, ITEM>> { public ITEM find(ID id, ImmutableItemManager<ID, ITEM> itemManager) throws InterruptedException, IOException { ITEM item; boolean cleanupPlaceholder = false; CacheEntry<ID, ITEM> entry = cache.get(id); if (entry != null && !entry.placeholder()) { entry.recentAccess(true); item = entry.item(); } else { try { synchronized (this) { while ((entry = cache.get(id)) != null && entry.placeholder()) { wait(); } if (entry == null) { assert cacheSize <= cacheCapacity; if (cacheSize == cacheCapacity) { // Cache is full. Evict something. ITEM victim = clock.takeItemToEvict(); assert victim != null; CacheEntry<ID, ITEM> removed = cache.remove(victim.id()); assert removed == victim : String.format("replaced: %s, entry: %s", removed, entry); itemManager.cleanupItemEvictedFromCache(victim); cacheSize--; } assert cacheSize < cacheCapacity; entry = ItemPlaceholder.<ID, ITEM>forCurrentThread(); CacheEntry<ID, ITEM> replaced = cache.put(id, entry); assert replaced == null; cacheSize++; } else { assert !entry.placeholder(); assert entry.id().equals(id); entry.recentAccess(true); } } if (entry.placeholder()) { // This thread wrote a placeholder. Read the cache item outside the lock, since this // is probably slow. assert entry.owner() == Thread.currentThread(); item = itemManager.getItemForCache(id); // Inside the lock, replace the placeholder with the item, and notify waiters. synchronized (this) { CacheEntry<ID, ITEM> replaced = cache.put(id, item); assert replaced == entry : String.format("replaced: %s, entry: %s", replaced, entry); clock.addItem(item); item.recentAccess(true); notifyAll(); } } else { entry.recentAccess(true); item = entry.item(); } } catch (RuntimeException | Error e) { cleanupPlaceholder = true; throw e; } finally { if (cleanupPlaceholder) { synchronized (this) { entry = cache.get(id); if (entry != null && entry.placeholder()) { cache.remove(id); } } } } } assert item.id().equals(id); return item; } public int size() { return cacheSize; } public ImmutableItemCache(int cacheCapacity) { this(cacheCapacity, new CacheEntryList.Observer<ID, ITEM>() { public void adding(ITEM item) { } public void evicting(ITEM victim) { } }); } public ImmutableItemCache(int cacheCapacity, CacheEntryList.Observer<ID, ITEM> observer) { this.cacheCapacity = cacheCapacity; this.cache = new ConcurrentHashMap<>(cacheCapacity); this.clock = new CacheEntryList<>(observer); } // For testing Map<ID, CacheEntry<ID, ITEM>> cacheContents() { return cache; } public void clear() { cache.clear(); cacheSize = 0; clock.clear(); } // Object state private final int cacheCapacity; private final Map<ID, CacheEntry<ID, ITEM>> cache; private volatile int cacheSize = 0; // Maintained here because cache.size() is O(n) private final CacheEntryList<ID, ITEM> clock; // The "clock" of the clock algorithm }