/* * Copyright Terracotta, Inc. * * 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 org.ehcache.clustered.server.offheap; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.locks.Lock; import org.ehcache.clustered.common.internal.store.Chain; import org.ehcache.clustered.common.internal.store.Element; import org.ehcache.clustered.common.internal.store.Util; import org.terracotta.offheapstore.MapInternals; import org.terracotta.offheapstore.ReadWriteLockedOffHeapClockCache; import org.terracotta.offheapstore.eviction.EvictionListener; import org.terracotta.offheapstore.eviction.EvictionListeningReadWriteLockedOffHeapClockCache; import org.terracotta.offheapstore.exceptions.OversizeMappingException; import org.terracotta.offheapstore.paging.PageSource; import org.terracotta.offheapstore.storage.portability.Portability; public class OffHeapChainMap<K> implements MapInternals { interface ChainMapEvictionListener<K> { void onEviction(K key); } private final ReadWriteLockedOffHeapClockCache<K, InternalChain> heads; private final OffHeapChainStorageEngine<K> chainStorage; private volatile ChainMapEvictionListener<K> evictionListener; public OffHeapChainMap(PageSource source, Portability<? super K> keyPortability, int minPageSize, int maxPageSize, boolean shareByThieving) { this.chainStorage = new OffHeapChainStorageEngine<>(source, keyPortability, minPageSize, maxPageSize, shareByThieving, shareByThieving); EvictionListener<K, InternalChain> listener = new EvictionListener<K, InternalChain>() { @Override public void evicting(Callable<Map.Entry<K, InternalChain>> callable) { try { Map.Entry<K, InternalChain> entry = callable.call(); try { if (evictionListener != null) { evictionListener.onEviction(entry.getKey()); } } finally { entry.getValue().close(); } } catch (Exception e) { throw new AssertionError(e); } } }; //TODO: EvictionListeningReadWriteLockedOffHeapClockCache lacks ctor that takes shareByThieving // this.heads = new ReadWriteLockedOffHeapClockCache<K, InternalChain>(source, shareByThieving, chainStorage); this.heads = new EvictionListeningReadWriteLockedOffHeapClockCache<>(listener, source, chainStorage); } //For tests OffHeapChainMap(ReadWriteLockedOffHeapClockCache<K, InternalChain> heads, OffHeapChainStorageEngine<K> chainStorage) { this.chainStorage = chainStorage; this.heads = heads; } void setEvictionListener(ChainMapEvictionListener<K> listener) { evictionListener = listener; } public Chain get(K key) { final Lock lock = heads.readLock(); lock.lock(); try { InternalChain chain = heads.get(key); if (chain == null) { return EMPTY_CHAIN; } else { try { return chain.detach(); } finally { chain.close(); } } } finally { lock.unlock(); } } public Chain getAndAppend(K key, ByteBuffer element) { final Lock lock = heads.writeLock(); lock.lock(); try { while (true) { InternalChain chain = heads.get(key); if (chain == null) { heads.put(key, chainStorage.newChain(element)); return EMPTY_CHAIN; } else { try { Chain current = chain.detach(); if (chain.append(element)) { return current; } else { evict(); } } finally { chain.close(); } } } } finally { lock.unlock(); } } public void append(K key, ByteBuffer element) { final Lock lock = heads.writeLock(); lock.lock(); try { while (true) { InternalChain chain = heads.get(key); if (chain == null) { heads.put(key, chainStorage.newChain(element)); return; } else { try { if (chain.append(element)) { return; } else { evict(); } } finally { chain.close(); } } } } finally { lock.unlock(); } } public void replaceAtHead(K key, Chain expected, Chain replacement) { final Lock lock = heads.writeLock(); lock.lock(); try { while (true) { InternalChain chain = heads.get(key); if (chain == null) { if (expected.isEmpty()) { throw new IllegalArgumentException("Empty expected sequence"); } else { return; } } else { try { if (chain.replace(expected, replacement)) { return; } else { evict(); } } finally { chain.close(); } } } } finally { lock.unlock(); } } public void put(K key, Chain chain) { final Lock lock = heads.writeLock(); lock.lock(); try { InternalChain current = heads.get(key); if (current != null) { try { replaceAtHead(key, current.detach(), chain); } finally { current.close(); } } else { for (Element x : chain) { append(key, x.getPayload()); } } } finally { lock.unlock(); } } public void clear() { heads.writeLock().lock(); try { this.heads.clear(); } finally { heads.writeLock().unlock(); } } public Set<K> keySet() { heads.writeLock().lock(); try { return heads.keySet(); } finally { heads.writeLock().unlock(); } } private void evict() { int evictionIndex = heads.getEvictionIndex(); if (evictionIndex < 0) { StringBuilder sb = new StringBuilder("Storage Engine and Eviction Failed - Everything Pinned ("); sb.append(getSize()).append(" mappings) \n").append("Storage Engine : ").append(chainStorage); throw new OversizeMappingException(sb.toString()); } else { heads.evict(evictionIndex, false); } } private static final Chain EMPTY_CHAIN = new Chain() { @Override public Iterator<Element> reverseIterator() { return Collections.<Element>emptyList().iterator(); } @Override public boolean isEmpty() { return true; } @Override public Iterator<Element> iterator() { return Collections.<Element>emptyList().iterator(); } }; public static Chain chain(ByteBuffer... buffers) { final List<Element> list = new ArrayList<Element>(); for (ByteBuffer b : buffers) { list.add(element(b)); } return new Chain() { final List<Element> elements = Collections.unmodifiableList(list); @Override public Iterator<Element> iterator() { return elements.iterator(); } @Override public Iterator<Element> reverseIterator() { return Util.reverseIterator(elements); } @Override public boolean isEmpty() { return elements.isEmpty(); } }; } private static Element element(final ByteBuffer b) { return new Element() { @Override public ByteBuffer getPayload() { return b.asReadOnlyBuffer(); } }; } @Override public long getSize() { return heads.getSize(); } @Override public long getTableCapacity() { return heads.getTableCapacity(); } @Override public long getUsedSlotCount() { return heads.getUsedSlotCount(); } @Override public long getRemovedSlotCount() { return heads.getRemovedSlotCount(); } @Override public int getReprobeLength() { return heads.getReprobeLength(); } @Override public long getAllocatedMemory() { return heads.getAllocatedMemory(); } @Override public long getOccupiedMemory() { return heads.getOccupiedMemory(); } @Override public long getVitalMemory() { return heads.getVitalMemory(); } @Override public long getDataAllocatedMemory() { return heads.getDataAllocatedMemory(); } @Override public long getDataOccupiedMemory() { return heads.getDataOccupiedMemory(); } @Override public long getDataVitalMemory() { return heads.getDataVitalMemory(); } @Override public long getDataSize() { return heads.getDataSize(); } boolean shrink() { return heads.shrink(); } Lock writeLock() { return heads.writeLock(); } protected void storageEngineFailure(Object failure) { } }