OHCacheBuilder.java

/*
 *      Copyright (C) 2014 Robert Stupp, Koeln, Germany, robert-stupp.de
 *
 *   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.caffinitas.ohc;

import java.util.concurrent.ScheduledExecutorService;

import org.caffinitas.ohc.chunked.OHCacheChunkedImpl;
import org.caffinitas.ohc.linked.OHCacheLinkedImpl;

/**
 * Configures and builds OHC instance.
 * <table summary="Configuration parameters">
 *     <tr>
 *         <th>Field</th>
 *         <th>Meaning</th>
 *         <th>Default</th>
 *     </tr>
 *     <tr>
 *         <td>{@code keySerializer}</td>
 *         <td>Serializer implementation used for keys</td>
 *         <td>Must be configured</td>
 *     </tr>
 *     <tr>
 *         <td>{@code valueSerializer}</td>
 *         <td>Serializer implementation used for values</td>
 *         <td>Must be configured</td>
 *     </tr>
 *     <tr>
 *         <td>{@code executorService}</td>
 *         <td>Executor service required for get operations using a cache loader. E.g. {@link org.caffinitas.ohc.OHCache#getWithLoaderAsync(Object, CacheLoader)}</td>
 *         <td>(Not configured by default meaning get operations with cache loader not supported by default)</td>
 *     </tr>
 *     <tr>
 *         <td>{@code segmentCount}</td>
 *         <td>Number of segments</td>
 *         <td>2 * number of CPUs ({@code java.lang.Runtime.availableProcessors()})</td>
 *     </tr>
 *     <tr>
 *         <td>{@code hashTableSize}</td>
 *         <td>Initial size of each segment's hash table</td>
 *         <td>{@code 8192}</td>
 *     </tr>
 *     <tr>
 *         <td>{@code loadFactor}</td>
 *         <td>Hash table load factor. I.e. determines when rehashing occurs.</td>
 *         <td>{@code .75f}</td>
 *     </tr>
 *     <tr>
 *         <td>{@code capacity}</td>
 *         <td>Capacity of the cache in bytes</td>
 *         <td>16 MB * number of CPUs ({@code java.lang.Runtime.availableProcessors()}), minimum 64 MB</td>
 *     </tr>
 *     <tr>
 *         <td>{@code chunkSize}</td>
 *         <td>If set and positive, the <i>chunked</i> implementation will be used and each segment
 *         will be divided into this amount of chunks.</td>
 *         <td>{@code 0} - i.e. <i>linked</i> implementation will be used</td>
 *     </tr>
 *     <tr>
 *         <td>{@code fixedEntrySize}</td>
 *         <td>If set and positive, the <i>chunked</i> implementation with fixed sized entries
 *         will be used. The parameter {@code chunkSize} must be set for fixed-sized entries.</td>
 *         <td>{@code 0} - i.e. <i>linked</i> implementation will be used,
 *         if {@code chunkSize} is also {@code 0}</td>
 *     </tr>
 *     <tr>
 *         <td>{@code maxEntrySize}</td>
 *         <td>Maximum size of a hash entry (including header, serialized key + serialized value)</td>
 *         <td>(not set, defaults to capacity divided by number of segments)</td>
 *     </tr>
 *     <tr>
 *         <td>{@code throwOOME}</td>
 *         <td>Throw {@code OutOfMemoryError} if off-heap allocation fails</td>
 *         <td>{@code false}</td>
 *     </tr>
 *     <tr>
 *         <td>{@code hashAlgorighm}</td>
 *         <td>Hash algorithm to use internally. Valid options are: {@code XX} for xx-hash, {@code MURMUR3} or {@code CRC32}
 *         Note: this setting does may only help to improve throughput in rare situations - i.e. if the key is
 *         very long and you've proven that it really improves performace</td>
 *         <td>{@code MURMUR3}</td>
 *     </tr>
 *     <tr>
 *         <td>{@code unlocked}</td>
 *         <td>If set to {@code true}, implementations will not perform any locking. The calling code has to take
 *         care of synchronized access. In order to create an instance for a thread-per-core implementation,
 *         set {@code segmentCount=1}, too.</td>
 *         <td>{@code false}</td>
 *     </tr>
 *     <tr>
 *         <td>{@code defaultTTLmillis}</td>
 *         <td>If set to a value {@code > 0}, implementations supporting TTLs will tag all entries with
 *         the given TTL in <b>milliseconds</b>.</td>
 *         <td>{@code 0}</td>
 *     </tr>
 *     <tr>
 *         <td>{@code timeoutsSlots}</td>
 *         <td>The number of timeouts slots for each segment - compare with hashed wheel timer.</td>
 *         <td>{@code 64}</td>
 *     </tr>
 *     <tr>
 *         <td>{@code timeoutsPrecision}</td>
 *         <td>The amount of time in milliseconds for each timeouts-slot.</td>
 *         <td>{@code 128}</td>
 *     </tr>
 *     <tr>
 *         <td>{@code ticker}</td>
 *         <td>Indirection for current time - used for unit tests.</td>
 *         <td>Default ticker using {@code System.nanoTime()} and {@code System.currentTimeMillis()}</td>
 *     </tr>
 *     <tr>
 *         <td>{@code eviction}</td>
 *         <td>Choose the eviction algorithm to use. Available are:
 *         <ul>
 *             <li>{@link Eviction#LRU LRU}: Plain LRU - least used entry is subject to eviction</li>
 *             <li>{@link Eviction#W_TINY_LFU W-WinyLFU}: Enable use of Window Tiny-LFU. The size of the
 *             frequency sketch ("admission filter") is set to the value of {@code hashTableSize}.
 *             See <a href="http://highscalability.com/blog/2016/1/25/design-of-a-modern-cache.html">this article</a>
 *             for a description.</li>
 *             <li>{@link Eviction#NONE None}: No entries will be evicted - this effectively provides a
 *             capacity-bounded off-heap map.</li>
 *         </ul>
 *         </td>
 *         <td>{@code LRU}</td>
 *     </tr>
 *     <tr>
 *         <td>{@code frequencySketchSize}</td>
 *         <td>Size of the frequency sketch used by {@link Eviction#W_TINY_LFU W-WinyLFU}</td>
 *         <td>Defaults to {@code hashTableSize}.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code edenSize}</td>
 *         <td>Size of the eden generation used by {@link Eviction#W_TINY_LFU W-WinyLFU} relative to a segment's size</td>
 *         <td>{@code 0.2}</td>
 *     </tr>
 * </table>
 * <p>
 *     You may also use system properties prefixed with {@code org.caffinitas.org.} to other defaults.
 *     E.g. the system property {@code org.caffinitas.org.segmentCount} configures the default of the number of segments.
 * </p>
 *
 * @param <K> cache key type
 * @param <V> cache value type
 */
public class OHCacheBuilder<K, V>
{
    private int segmentCount;
    private int hashTableSize = 8192;
    private long capacity;
    private int chunkSize;
    private CacheSerializer<K> keySerializer;
    private CacheSerializer<V> valueSerializer;
    private float loadFactor = .75f;
    private int fixedKeySize;
    private int fixedValueSize;
    private long maxEntrySize;
    private ScheduledExecutorService executorService;
    private boolean throwOOME;
    private HashAlgorithm hashAlgorighm = HashAlgorithm.MURMUR3;
    private boolean unlocked;
    private long defaultTTLmillis;
    private boolean timeouts;
    private int timeoutsSlots;
    private int timeoutsPrecision;
    private Ticker ticker = Ticker.DEFAULT;
    private Eviction eviction = Eviction.LRU;
    private int frequencySketchSize;
    private double edenSize = 0.2d;

    private OHCacheBuilder()
    {
        int cpus = Runtime.getRuntime().availableProcessors();

        segmentCount = roundUpToPowerOf2(cpus * 2, 1 << 30);
        capacity = Math.min(cpus * 16, 64) * 1024 * 1024;

        segmentCount = fromSystemProperties("segmentCount", segmentCount);
        hashTableSize = fromSystemProperties("hashTableSize", hashTableSize);
        capacity = fromSystemProperties("capacity", capacity);
        chunkSize = fromSystemProperties("chunkSize", chunkSize);
        loadFactor = fromSystemProperties("loadFactor", loadFactor);
        maxEntrySize = fromSystemProperties("maxEntrySize", maxEntrySize);
        throwOOME = fromSystemProperties("throwOOME", throwOOME);
        hashAlgorighm = HashAlgorithm.valueOf(fromSystemProperties("hashAlgorighm", hashAlgorighm.name()));
        unlocked = fromSystemProperties("unlocked", unlocked);
        defaultTTLmillis = fromSystemProperties("defaultTTLmillis", defaultTTLmillis);
        timeouts = fromSystemProperties("timeouts", timeouts);
        timeoutsSlots = fromSystemProperties("timeoutsSlots", timeoutsSlots);
        timeoutsPrecision = fromSystemProperties("timeoutsPrecision", timeoutsPrecision);
        eviction = fromSystemProperties("eviction", eviction, Eviction.class);
        frequencySketchSize = fromSystemProperties("frequencySketchSize", frequencySketchSize);
        edenSize = fromSystemProperties("edenSize", edenSize);
    }

    public static final String SYSTEM_PROPERTY_PREFIX = "org.caffinitas.ohc.";

    private static float fromSystemProperties(String name, float defaultValue)
    {
        try
        {
            return Float.parseFloat(System.getProperty(SYSTEM_PROPERTY_PREFIX + name, Float.toString(defaultValue)));
        }
        catch (Exception e)
        {
            throw new RuntimeException("Failed to parse system property " + SYSTEM_PROPERTY_PREFIX + name, e);
        }
    }

    private static long fromSystemProperties(String name, long defaultValue)
    {
        try
        {
            return Long.parseLong(System.getProperty(SYSTEM_PROPERTY_PREFIX + name, Long.toString(defaultValue)));
        }
        catch (Exception e)
        {
            throw new RuntimeException("Failed to parse system property " + SYSTEM_PROPERTY_PREFIX + name, e);
        }
    }

    private static int fromSystemProperties(String name, int defaultValue)
    {
        try
        {
            return Integer.parseInt(System.getProperty(SYSTEM_PROPERTY_PREFIX + name, Integer.toString(defaultValue)));
        }
        catch (Exception e)
        {
            throw new RuntimeException("Failed to parse system property " + SYSTEM_PROPERTY_PREFIX + name, e);
        }
    }

    private static double fromSystemProperties(String name, double defaultValue)
    {
        try
        {
            return Double.parseDouble(System.getProperty(SYSTEM_PROPERTY_PREFIX + name, Double.toString(defaultValue)));
        }
        catch (Exception e)
        {
            throw new RuntimeException("Failed to parse system property " + SYSTEM_PROPERTY_PREFIX + name, e);
        }
    }

    private static boolean fromSystemProperties(String name, boolean defaultValue)
    {
        try
        {
            return Boolean.parseBoolean(System.getProperty(SYSTEM_PROPERTY_PREFIX + name, Boolean.toString(defaultValue)));
        }
        catch (Exception e)
        {
            throw new RuntimeException("Failed to parse system property " + SYSTEM_PROPERTY_PREFIX + name, e);
        }
    }

    private static String fromSystemProperties(String name, String defaultValue)
    {
        return System.getProperty(SYSTEM_PROPERTY_PREFIX + name, defaultValue);
    }

    private static <E extends Enum> E fromSystemProperties(String name, E defaultValue, Class<E> type)
    {
        String value = fromSystemProperties(name, defaultValue.name());
        return (E) Enum.valueOf(type, value.toUpperCase());
    }

    static int roundUpToPowerOf2(int number, int max)
    {
        return number >= max
               ? max
               : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

    public static <K, V> OHCacheBuilder<K, V> newBuilder()
    {
        return new OHCacheBuilder<>();
    }

    public OHCache<K, V> build()
    {
        if (fixedKeySize > 0 || fixedValueSize > 0|| chunkSize > 0)
            return new OHCacheChunkedImpl<>(this);
        return new OHCacheLinkedImpl<>(this);
    }

    public int getHashTableSize()
    {
        return hashTableSize;
    }

    public OHCacheBuilder<K, V> hashTableSize(int hashTableSize)
    {
        if (hashTableSize < -1)
            throw new IllegalArgumentException("hashTableSize:" + hashTableSize);
        this.hashTableSize = hashTableSize;
        return this;
    }

    public int getChunkSize()
    {
        return chunkSize;
    }

    public OHCacheBuilder<K, V> chunkSize(int chunkSize)
    {
        if (chunkSize < -1)
            throw new IllegalArgumentException("chunkSize:" + chunkSize);
        this.chunkSize = chunkSize;
        return this;
    }

    public long getCapacity()
    {
        return capacity;
    }

    public OHCacheBuilder<K, V> capacity(long capacity)
    {
        if (capacity <= 0)
            throw new IllegalArgumentException("capacity:" + capacity);
        this.capacity = capacity;
        return this;
    }

    public CacheSerializer<K> getKeySerializer()
    {
        return keySerializer;
    }

    public OHCacheBuilder<K, V> keySerializer(CacheSerializer<K> keySerializer)
    {
        this.keySerializer = keySerializer;
        return this;
    }

    public CacheSerializer<V> getValueSerializer()
    {
        return valueSerializer;
    }

    public OHCacheBuilder<K, V> valueSerializer(CacheSerializer<V> valueSerializer)
    {
        this.valueSerializer = valueSerializer;
        return this;
    }

    public int getSegmentCount()
    {
        return segmentCount;
    }

    public OHCacheBuilder<K, V> segmentCount(int segmentCount)
    {
        if (segmentCount < -1)
            throw new IllegalArgumentException("segmentCount:" + segmentCount);
        this.segmentCount = segmentCount;
        return this;
    }

    public float getLoadFactor()
    {
        return loadFactor;
    }

    public OHCacheBuilder<K, V> loadFactor(float loadFactor)
    {
        if (loadFactor <= 0f)
            throw new IllegalArgumentException("loadFactor:" + loadFactor);
        this.loadFactor = loadFactor;
        return this;
    }

    public long getMaxEntrySize()
    {
        return maxEntrySize;
    }

    public OHCacheBuilder<K, V> maxEntrySize(long maxEntrySize)
    {
        if (maxEntrySize < 0)
            throw new IllegalArgumentException("maxEntrySize:" + maxEntrySize);
        this.maxEntrySize = maxEntrySize;
        return this;
    }

    public int getFixedKeySize()
    {
        return fixedKeySize;
    }

    public int getFixedValueSize()
    {
        return fixedValueSize;
    }

    public OHCacheBuilder<K, V> fixedEntrySize(int fixedKeySize, int fixedValueSize)
    {
        if ((fixedKeySize > 0 || fixedValueSize > 0) &&
            (fixedKeySize <= 0 || fixedValueSize <= 0))
            throw new IllegalArgumentException("fixedKeySize:" + fixedKeySize+",fixedValueSize:" + fixedValueSize);
        this.fixedKeySize = fixedKeySize;
        this.fixedValueSize = fixedValueSize;
        return this;
    }

    public ScheduledExecutorService getExecutorService()
    {
        return executorService;
    }

    public OHCacheBuilder<K, V> executorService(ScheduledExecutorService executorService)
    {
        this.executorService = executorService;
        return this;
    }

    public HashAlgorithm getHashAlgorighm()
    {
        return hashAlgorighm;
    }

    public OHCacheBuilder<K, V> hashMode(HashAlgorithm hashMode)
    {
        if (hashMode == null)
            throw new NullPointerException("hashMode");
        this.hashAlgorighm = hashMode;
        return this;
    }

    public boolean isThrowOOME()
    {
        return throwOOME;
    }

    public OHCacheBuilder<K, V> throwOOME(boolean throwOOME)
    {
        this.throwOOME = throwOOME;
        return this;
    }

    public boolean isUnlocked()
    {
        return unlocked;
    }

    public OHCacheBuilder<K, V> unlocked(boolean unlocked)
    {
        this.unlocked = unlocked;
        return this;
    }

    public long getDefaultTTLmillis()
    {
        return defaultTTLmillis;
    }

    public OHCacheBuilder<K, V> defaultTTLmillis(long defaultTTLmillis)
    {
        this.defaultTTLmillis = defaultTTLmillis;
        return this;
    }

    public boolean isTimeouts()
    {
        return timeouts;
    }

    public OHCacheBuilder<K, V> timeouts(boolean timeouts)
    {
        this.timeouts = timeouts;
        return this;
    }

    public int getTimeoutsSlots()
    {
        return timeoutsSlots;
    }

    public OHCacheBuilder<K, V> timeoutsSlots(int timeoutsSlots)
    {
        if (timeoutsSlots > 0)
            this.timeouts = true;
        this.timeoutsSlots = timeoutsSlots;
        return this;
    }

    public int getTimeoutsPrecision()
    {
        return timeoutsPrecision;
    }

    public OHCacheBuilder<K, V> timeoutsPrecision(int timeoutsPrecision)
    {
        if (timeoutsPrecision > 0)
            this.timeouts = true;
        this.timeoutsPrecision = timeoutsPrecision;
        return this;
    }

    public Ticker getTicker()
    {
        return ticker;
    }

    public OHCacheBuilder<K, V> ticker(Ticker ticker)
    {
        this.ticker = ticker;
        return this;
    }

    public Eviction getEviction()
    {
        return eviction;
    }

    public OHCacheBuilder<K, V> eviction(Eviction eviction)
    {
        this.eviction = eviction;
        return this;
    }

    public int getFrequencySketchSize()
    {
        return frequencySketchSize;
    }

    public OHCacheBuilder<K, V> frequencySketchSize(int frequencySketchSize)
    {
        this.frequencySketchSize = frequencySketchSize;
        return this;
    }

    public double getEdenSize()
    {
        return edenSize;
    }

    public OHCacheBuilder<K, V> edenSize(double edenSize)
    {
        this.edenSize = edenSize;
        return this;
    }
}