Newer
Older
uid.tools / src / main / java / ua / net / uid / utils / io / ExpiringCache.java
@Vladimir Burdo Vladimir Burdo on 5 Sep 2019 16 KB ...
/*
 * Copyright 2019 nightfall.
 *
 * 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 ua.net.uid.utils.io;

import java.util.Date;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;

public class ExpiringCache<K, V> {
    ////////////////////////////////////////////////////////////////////////////
    public static final int MAX_TABLE_SIZE = Integer.MAX_VALUE >>> 3;
    public static final int INIT_SIZE = 64;
    public static final float LOAD_FACTOR = 7F/3F;
    ////////////////////////////////////////////////////////////////////////////
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final AtomicInteger count = new AtomicInteger(0);
    private final AtomicInteger modified = new AtomicInteger(0);
    private final float loadFactor;
    private int nextResize;
    private Chain<K, V>[] table;
    ////////////////////////////////////////////////////////////////////////////
    public ExpiringCache() {
        this(INIT_SIZE, LOAD_FACTOR);
    }
    public ExpiringCache(int initSize) {
        this(initSize, LOAD_FACTOR);
    }
    public ExpiringCache(float loadFactor) {
        this(INIT_SIZE, loadFactor);
    }
    public ExpiringCache(int initSize, float loadFactor) {
        this.loadFactor = loadFactor;
        this.nextResize = (int)(loadFactor * initSize);
        this.table = create(initSize);
    }
    ////////////////////////////////////////////////////////////////////////////
    public int getSize() { return count.get(); }
    
    public V get(K key) {
        return get(key, now());
    }
    
    public V get(K key, long now) {
        return chain(key.hashCode()).get(key, now);
    }

    public V get(K key, Function<K, V> callback, long ttl) {
        return get(key, callback, ttl, now());
    }
    
    public V get(K key, Function<K, V> callback, Date expiry) {
        return get(key, callback, expiry, now());
    }

    public V get(K key, Function<K, V> callback, long ttl, long now) {
        return chain(key.hashCode()).get(key, callback, now + ttl, now);
    }
    
    public V get(K key, Function<K, V> callback, Date expiry, long now) {
        return chain(key.hashCode()).get(key, callback, expiry.getTime(), now);
    }
    
    public void set(K key, V value, long ttl) {
        set(key, value, ttl, now());
    }
    
    public void set(K key, V value, Date expiry) {
        set(key, value, expiry, now());
    }

    public void set(K key, V value, long ttl, long now) {
        chain(key.hashCode()).set(key, value, ttl + now, now);
    }
    
    public void set(K key, V value, Date expiry, long now) {
        chain(key.hashCode()).set(key, value, expiry.getTime(), now);
    }
    
    public void remove(K key) {
        remove(key, now());
    }
    
    public void remove(K key, long now) {
        chain(key.hashCode()).remove(key, now);
    }
    
    public V extract(K key) {
        return extract(key, now());
    }
    
    public V extract(K key, long now) {
        return chain(key.hashCode()).extract(key, now);
    }
    
    public void gc() {
        lock.readLock().lock();
        try {
            long now = now();
            for(Chain<K, V> chain: table)
                chain.gc(now);
            modified.set(0);
        } finally {
            lock.readLock().unlock();
        }
    }
    /*
    //TODO public void pack() 
    public void pack() {
        lock.writeLock().lock();
        try {
            throw new UnsupportedOperationException("TODO: Not supported yet.");
        } finally {
            lock.writeLock().unlock();
        }
    }
    */
    public void empty() {
        lock.readLock().lock();
        try {
            for(Chain<K, V> chain: table)
                chain.next = null;
            modified.set(0);
        } finally {
            lock.readLock().unlock();
        }
    }

    public void clear(int initSize) {
        lock.writeLock().lock();
        try {
            table = create(initSize);
            modified.set(0);
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void clear() {
        clear(INIT_SIZE);
    }
    ////////////////////////////////////////////////////////////////////////////
    private static long now() { return System.currentTimeMillis(); }

    private Chain<K, V>[] create(int size) {
        Chain<K, V>[] result = (Chain<K, V>[]) new Chain[size];
        for(int i = 0; i < size; ++i)
            result[i] = new Chain<>(this);
        return result;
    }

    private static int index(int hash, int length) {
        return (hash & MAX_TABLE_SIZE) % length;
    }

    private Chain<K, V> chain(int hash) {
        lock.readLock().lock();
        try {
            return table[index(hash, table.length)];
        } finally {
            lock.readLock().unlock();
        }
    }
    
    private void resize(int delta) {
        int newSize = Math.min(MAX_TABLE_SIZE, count.addAndGet(delta));
        if (newSize > nextResize) {
            lock.writeLock().lock();
            try {
                Chain<K, V>[] result = create(newSize);
                int newCount = 0;
                long now = now();
                for(Chain<K, V> src : table) {
                    Entry<K, V> old = src.next;
                    while (old != null && old.expiry > now) {
                        result[index(old.key.hashCode(), newSize)].add(old.key, old.value, old.expiry);
                        ++newCount;
                        old = old.next;
                    }
                    src.next = null;
                }
                table = result;
                count.set(newCount);
                modified.set(0);
                nextResize = Math.min(MAX_TABLE_SIZE, (int)(loadFactor * newSize));
            } finally {
                lock.writeLock().unlock();
            }
        } else {
            if (modified.incrementAndGet() > nextResize)
                gc();
        }
    }
    ////////////////////////////////////////////////////////////////////////////
    private static abstract class Item<K, V> {
        Entry<K, V> next;
        Item(Entry<K, V> next) { this.next = next; }
    }
    ////////////////////////////////////////////////////////////////////////////
    private static final class Entry<K, V> extends Item<K, V> {
        final K key;
        final V value;
        final long expiry;
        Entry(K key, V value, long expiry, Entry<K, V> next) {
            super(next);
            this.expiry = expiry;
            this.key = key;
            this.value = value;
        }
    }
    ////////////////////////////////////////////////////////////////////////////
    private static final class Chain<K, V> extends Item<K, V> {
        final ExpiringCache<K, V> cache;
        int count = 0;
        
        Chain(ExpiringCache<K, V> cache) {
            super(null);
            this.cache = cache;
        }
        
        private void count(int value) {
            if (count != value) {
                int old = count;
                count = value;
                cache.resize(value - old);
            }
        }

        private boolean checkNext(Item<K, V> current, long now) {
            if (current.next != null) {
                if (current.next.expiry <= now) {
                    current.next = null;
                } else {
                    return true;
                }
            }
            return false;
        }

        void add(K key, V value, long expiry) {
            Item<K, V> item = this;
            while (item.next != null && item.next.expiry > expiry) {
                item = item.next;
            }
            item.next = new Entry<>(key, value, expiry, item.next);
            ++count;
        }

        synchronized V get(K key, long now) {
            Item<K, V> item = this;
            int cnt = 0;
            V result = null;
            while (checkNext(item, now)) {
                if (Objects.equals(key, item.next.key))
                    result = item.next.value;
                ++cnt;
                item = item.next;
            }
            count(cnt);
            return result;
        }

        synchronized V get(K key, Function<K, V> callback, long expiry, long now) {
            Item<K, V> item = this, last = this;
            int cnt = 0;
            while (checkNext(item, now)) {
                ++cnt;
                if (Objects.equals(key, item.next.key)) {
                    V value = item.next.value;
                    while (checkNext(item, now)) {
                        ++cnt;
                        item = item.next;
                    }
                    count(cnt);
                    return value;
                }
                if (item.next.expiry > expiry)
                    last = item.next;
                item = item.next;
            }
            V value = callback.apply(key);
            last.next = new Entry<>(key, value, expiry, last.next);
            count(cnt + 1);
            return value;
        }

        synchronized void set(K key, V value, long expiry, long now) {
            Item<K, V> item = this, last = this;
            int cnt = 0;
            while (checkNext(item, now)) {
                if (Objects.equals(key, item.next.key)) {
                    item.next = item.next.next;
                    while (checkNext(item, now)) {
                        ++cnt;
                        if (item.next.expiry > expiry)
                            last = item.next;
                        item = item.next;
                    }
                    break;
                }
                ++cnt;
                if (item.next.expiry > expiry)
                    last = item.next;
                item = item.next;
            }
            last.next = new Entry<>(key, value, expiry, last.next);
            count(cnt + 1);
        }

        synchronized void remove(K key, long now) {
            Item<K, V> item = this;
            int cnt = 0;
            while (checkNext(item, now)) {
                if (Objects.equals(key, item.next.key)) {
                    item.next = item.next.next;
                } else {
                    ++cnt;
                    item = item.next;
                }
            }
            count(cnt);
        }
        
        synchronized V extract(K key, long now) {
            Item<K, V> item = this;
            int cnt = 0;
            V result = null;
            while (checkNext(item, now)) {
                if (Objects.equals(key, item.next.key)) {
                    result = item.next.value;
                    item.next = item.next.next;
                } else {
                    ++cnt;
                    item = item.next;
                }
            }
            count(cnt);
            return result;
        }

        synchronized void gc(long now) {
            Item<K, V> item = this;
            int cnt = 0;
            while (checkNext(item, now)) {
                ++cnt;
                item = item.next;
            }
            count(cnt);
        }
    }
    ////////////////////////////////////////////////////////////////////////////
    
    
    
    /*
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    private final static class Chain<K, V> extends Item<K, V> {
        private final ExpiringCache<K, V> cache;
        private int count = 0;

        Chain(ExpiringCache<K, V> cache, Entry<K, V> next) {
            super(next);
            this.cache = cache;
        }

        synchronized Future<V> get(long now, K key) {
            Item<K, V> item = this;
            int cnt = 0;
            while (next(now, item)) {
                if (Objects.equals(key, item.next.key)) {
                    return item.next.future;
                }
                ++cnt;
                item = item.next;
            }
            count(cnt);
            return null;
        }

        synchronized Future<V> future(long now, K key, Callable<V> callable, long expiry) {
            Item<K, V> item = this, last = this;
            int cnt = 0;
            while (next(now, item)) {
                if (Objects.equals(key, item.next.key)) {
                    return item.next.future;
                }
                ++cnt;
                if (item.next.expiry > expiry) {
                    last = item.next;
                }
                item = item.next;
            }
            last.next = new Entry<>(key, cache.executor.submit(callable), expiry, last.next);
            count(cnt + 1);
            return last.next.future;
        }

        synchronized void set(long now, K key, V value, long expiry) {
            Item<K, V> item = this, last = this;
            int cnt = 0;
            while (next(now, item)) {
                if (Objects.equals(key, item.next.key)) {
                    item.next = item.next.next;
                    continue;
                }
                ++cnt;
                if (item.next.expiry > expiry) {
                    last = item.next;
                }
                item = item.next;
            }
            last.next = new Entry<>(key, new ReadyFuture<>(value), expiry, last.next);
            count(cnt + 1);
        }

        synchronized void put(long now, K key, Callable<V> callable, long expiry) {
            Item<K, V> item = this, last = this;
            int cnt = 0;
            while (next(now, item)) {
                if (Objects.equals(key, item.next.key)) {
                    item.next = item.next.next;
                    continue;
                }
                ++cnt;
                if (item.next.expiry > expiry) {
                    last = item.next;
                }
                item = item.next;
            }
            last.next = new Entry<>(key, cache.executor.submit(callable), expiry, last.next);
            count(cnt + 1);
        }

        synchronized Future<V> extract(long now, K key) {
            Item<K, V> item = this;
            int cnt = 0;
            while (next(now, item)) {
                if (Objects.equals(key, item.next.key)) {
                    try {
                        return item.next.future;
                    } finally {
                        item.next = item.next.next;
                        --count;
                        cache.count.decrementAndGet();
                    }
                }
                ++cnt;
                item = item.next;
            }
            count(cnt);
            return null;
        }

        synchronized void remove(long now, K key) {
            Item<K, V> item = this;
            int cnt = 0;
            while (next(now, item)) {
                if (Objects.equals(key, item.next.key)) {
                    item.next = item.next.next;
                    --count;
                    cache.count.decrementAndGet();
                    return;
                }
                ++cnt;
                item = item.next;
            }
            count(cnt);
        }

        void gc(long now) {
            Item<K, V> item = this;
            int cnt = 0;
            while (next(now, item)) {
                ++cnt;
                item = item.next;
            }
            count(cnt);
        }

        void  clear() {
            cache.count.addAndGet(-count);
            count = 0;
            next = null;
        }

        private boolean next(long now, Item<K, V> current) {
            if (current.next != null) {
                if (current.next.expiry <= now) {
                    current.next = null;
                } else {
                    return true;
                }
            }
            return false;
        }

        private void count(int value) {
            if (count != value) {
                cache.count.addAndGet(value - count);
                count = value;
            }
        }
    }*/
}